Merge "Stop sharing ProximitySensor in Doze code." into rvc-dev
diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp
index 3dbe413..0617eb6 100644
--- a/cmds/statsd/Android.bp
+++ b/cmds/statsd/Android.bp
@@ -116,7 +116,6 @@
         "libcutils",
         "libgtest_prod",
         "libprotoutil",
-        "libstatsmetadata",
         "libstatslog_statsd",
         "libsysutils",
         "libutils",
@@ -129,51 +128,6 @@
     ],
 }
 
-// ================
-// libstatsmetadata
-// ================
-
-genrule {
-    name: "atoms_info.h",
-    tools: ["stats-log-api-gen"],
-    cmd: "$(location stats-log-api-gen) --atomsInfoHeader $(genDir)/atoms_info.h",
-    out: [
-        "atoms_info.h",
-    ],
-}
-
-genrule {
-    name: "atoms_info.cpp",
-    tools: ["stats-log-api-gen"],
-    cmd: "$(location stats-log-api-gen) --atomsInfoCpp $(genDir)/atoms_info.cpp",
-    out: [
-        "atoms_info.cpp",
-    ],
-}
-
-cc_library_static {
-    name: "libstatsmetadata",
-    host_supported: true,
-    generated_sources: [
-        "atoms_info.cpp",
-    ],
-    generated_headers: [
-        "atoms_info.h",
-    ],
-    cflags: [
-        "-Wall",
-        "-Werror",
-    ],
-    export_generated_headers: [
-        "atoms_info.h",
-    ],
-    apex_available: [
-        //TODO(b/149782403): Remove this once statsd no longer links against libstatsmetadata
-        "com.android.os.statsd",
-        "test_com.android.os.statsd",
-    ],
-}
-
 genrule {
     name: "statslog_statsd.h",
     tools: ["stats-log-api-gen"],
diff --git a/cmds/statsd/src/atom_field_options.proto b/cmds/statsd/src/atom_field_options.proto
index 8527185..ff5717e 100644
--- a/cmds/statsd/src/atom_field_options.proto
+++ b/cmds/statsd/src/atom_field_options.proto
@@ -110,8 +110,6 @@
 
     optional LogMode log_mode = 50002 [default = MODE_AUTOMATIC];
 
-    optional bool allow_from_any_uid = 50003 [default = false];
-
     repeated string module = 50004;
 
     optional bool truncate_timestamp = 50005 [default = false];
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index dc20a02..a5f0ac9 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -145,8 +145,7 @@
         PacketWakeupOccurred packet_wakeup_occurred = 44 [(module) = "framework"];
         WallClockTimeShifted wall_clock_time_shifted = 45 [(module) = "framework"];
         AnomalyDetected anomaly_detected = 46 [(module) = "statsd"];
-        AppBreadcrumbReported app_breadcrumb_reported =
-                47 [(allow_from_any_uid) = true, (module) = "statsd"];
+        AppBreadcrumbReported app_breadcrumb_reported = 47 [(module) = "statsd"];
         AppStartOccurred app_start_occurred = 48 [(module) = "framework", (module) = "statsdtest"];
         AppStartCanceled app_start_canceled = 49 [(module) = "framework"];
         AppStartFullyDrawn app_start_fully_drawn = 50 [(module) = "framework"];
@@ -157,7 +156,7 @@
         AppStartMemoryStateCaptured app_start_memory_state_captured = 55 [(module) = "framework"];
         ShutdownSequenceReported shutdown_sequence_reported = 56 [(module) = "framework"];
         BootSequenceReported boot_sequence_reported = 57;
-        DaveyOccurred davey_occurred = 58 [(allow_from_any_uid) = true, (module) = "statsd"];
+        DaveyOccurred davey_occurred = 58 [(module) = "statsd"];
         OverlayStateChanged overlay_state_changed =
                 59 [(module) = "framework", (module) = "statsdtest"];
         ForegroundServiceStateChanged foreground_service_state_changed
@@ -186,8 +185,7 @@
         WTFOccurred wtf_occurred = 80 [(module) = "framework"];
         LowMemReported low_mem_reported = 81 [(module) = "framework"];
         GenericAtom generic_atom = 82;
-        KeyValuePairsAtom key_value_pairs_atom =
-                83 [(allow_from_any_uid) = true, (module) = "framework", (module) = "statsd"];
+        KeyValuePairsAtom key_value_pairs_atom = 83 [(module) = "framework", (module) = "statsd"];
         VibratorStateChanged vibrator_state_changed = 84 [(module) = "framework"];
         DeferredJobStatsReported deferred_job_stats_reported = 85 [(module) = "framework"];
         ThermalThrottlingStateChanged thermal_throttling = 86 [deprecated=true];
@@ -317,7 +315,7 @@
         AssistGestureFeedbackReported assist_gesture_feedback_reported = 175 [(module) = "sysui"];
         AssistGestureProgressReported assist_gesture_progress_reported = 176 [(module) = "sysui"];
         TouchGestureClassified touch_gesture_classified = 177 [(module) = "framework"];
-        HiddenApiUsed hidden_api_used = 178 [(allow_from_any_uid) = true, (module) = "framework"];
+        HiddenApiUsed hidden_api_used = 178 [(module) = "framework"];
         StyleUIChanged style_ui_changed = 179 [(module) = "sysui"];
         PrivacyIndicatorsInteracted privacy_indicators_interacted =
                 180 [(module) = "permissioncontroller"];
@@ -383,7 +381,7 @@
         UpdateEngineSuccessfulUpdateReported update_engine_successful_update_reported = 226;
         CameraActionEvent camera_action_event = 227 [(module) = "framework"];
         AppCompatibilityChangeReported app_compatibility_change_reported =
-            228 [(allow_from_any_uid) = true, (module) = "framework"];
+                228 [(module) = "framework"];
         PerfettoUploaded perfetto_uploaded = 229 [(module) = "perfetto"];
         VmsClientConnectionStateChanged vms_client_connection_state_changed =
                 230 [(module) = "car"];
@@ -486,6 +484,7 @@
         KeystoreKeyEventReported keystore_key_event_reported = 302;
         NetworkTetheringReported  network_tethering_reported =
             303 [(module) = "network_tethering"];
+        ImeTouchReported ime_touch_reported = 304 [(module) = "sysui"];
 
         // StatsdStats tracks platform atoms with ids upto 500.
         // Update StatsdStats::kMaxPushedAtomId when atom ids here approach that value.
@@ -3061,6 +3060,18 @@
 }
 
 /**
+ * Logs when IME is on.
+ *
+ * Logged from: /packages/SystemUI/src/com/android/systemui/
+                statusbar/phone/NavigationBarView.java
+ *
+ */
+message ImeTouchReported {
+    optional int32 x_coordinate = 1;  // X coordinate for ACTION_DOWN event.
+    optional int32 y_coordinate = 2;  // Y coordinate for ACTION_DOWN event.
+}
+
+/**
  * Logs when Launcher (HomeScreen) UI has changed or was interacted.
  *
  * Logged from:
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 7e825ef..60de1a2 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -21,7 +21,6 @@
 #include <private/android_filesystem_config.h>
 
 #include "CountMetricProducer.h"
-#include "atoms_info.h"
 #include "condition/CombinationConditionTracker.h"
 #include "condition/SimpleConditionTracker.h"
 #include "guardrail/StatsdStats.h"
@@ -372,13 +371,6 @@
 
 
 bool MetricsManager::checkLogCredentials(const LogEvent& event) {
-    // TODO(b/154856835): Remove this check once we get whitelist from the config.
-    if (android::util::AtomsInfo::kWhitelistedAtoms.find(event.GetTagId()) !=
-      android::util::AtomsInfo::kWhitelistedAtoms.end())
-    {
-        return true;
-    }
-
     if (mWhitelistedAtomIds.find(event.GetTagId()) != mWhitelistedAtomIds.end()) {
         return true;
     }
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 22a63d9..3772755 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -2816,6 +2816,11 @@
      * The system may disallow entering picture-in-picture in various cases, including when the
      * activity is not visible, if the screen is locked or if the user has an activity pinned.
      *
+     * <p>By default, system calculates the dimension of picture-in-picture window based on the
+     * given {@param params}.
+     * See <a href="{@docRoot}guide/topics/ui/picture-in-picture">Picture-in-picture Support</a>
+     * on how to override this behavior.</p>
+     *
      * @see android.R.attr#supportsPictureInPicture
      * @see PictureInPictureParams
      *
diff --git a/core/java/android/app/usage/NetworkStatsManager.java b/core/java/android/app/usage/NetworkStatsManager.java
index d6e7762..fc8248e 100644
--- a/core/java/android/app/usage/NetworkStatsManager.java
+++ b/core/java/android/app/usage/NetworkStatsManager.java
@@ -41,6 +41,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
+import android.telephony.TelephonyManager;
 import android.util.DataUnit;
 import android.util.Log;
 
@@ -198,6 +199,12 @@
      *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
      *            etc.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param startTime Start of period. Defined in terms of "Unix time", see
      *            {@link java.lang.System#currentTimeMillis}.
      * @param endTime End of period. Defined in terms of "Unix time", see
@@ -231,6 +238,12 @@
      *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
      *            etc.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param startTime Start of period. Defined in terms of "Unix time", see
      *            {@link java.lang.System#currentTimeMillis}.
      * @param endTime End of period. Defined in terms of "Unix time", see
@@ -268,6 +281,12 @@
      *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
      *            etc.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param startTime Start of period. Defined in terms of "Unix time", see
      *            {@link java.lang.System#currentTimeMillis}.
      * @param endTime End of period. Defined in terms of "Unix time", see
@@ -301,7 +320,7 @@
     /**
      * Query network usage statistics details for a given uid.
      *
-     * #see queryDetailsForUidTagState(int, String, long, long, int, int, int)
+     * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
      */
     public NetworkStats queryDetailsForUid(int networkType, String subscriberId,
             long startTime, long endTime, int uid) throws SecurityException {
@@ -319,7 +338,7 @@
     /**
      * Query network usage statistics details for a given uid and tag.
      *
-     * #see queryDetailsForUidTagState(int, String, long, long, int, int, int)
+     * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
      */
     public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId,
             long startTime, long endTime, int uid, int tag) throws SecurityException {
@@ -344,6 +363,12 @@
      *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
      *            etc.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param startTime Start of period. Defined in terms of "Unix time", see
      *            {@link java.lang.System#currentTimeMillis}.
      * @param endTime End of period. Defined in terms of "Unix time", see
@@ -398,6 +423,12 @@
      *            {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
      *            etc.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when querying for the mobile network type to receive usage
+     *                     for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param startTime Start of period. Defined in terms of "Unix time", see
      *            {@link java.lang.System#currentTimeMillis}.
      * @param endTime End of period. Defined in terms of "Unix time", see
@@ -455,7 +486,7 @@
     /**
      * Registers to receive notifications about data usage on specified networks.
      *
-     * #see registerUsageCallback(int, String[], long, UsageCallback, Handler)
+     * @see #registerUsageCallback(int, String, long, UsageCallback, Handler)
      */
     public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
             UsageCallback callback) {
@@ -472,6 +503,12 @@
      * @param networkType Type of network to monitor. Either
                   {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
      * @param subscriberId If applicable, the subscriber id of the network interface.
+     *                     <p>Starting with API level 29, the {@code subscriberId} is guarded by
+     *                     additional restrictions. Calling apps that do not meet the new
+     *                     requirements to access the {@code subscriberId} can provide a {@code
+     *                     null} value when registering for the mobile network type to receive
+     *                     notifications for all mobile networks. For additional details see {@link
+     *                     TelephonyManager#getSubscriberId()}.
      * @param thresholdBytes Threshold in bytes to be notified on.
      * @param callback The {@link UsageCallback} that the system will call when data usage
      *            has exceeded the specified threshold.
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 2ce6a86..8a6cc95 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -154,6 +154,7 @@
      * been misbehaving in some manner.
      * Apps in this bucket will have the most restrictions, including network restrictions and
      * additional restrictions on jobs.
+     * <p> Note: this bucket is not enabled in {@link Build.VERSION_CODES#R}.
      * @see #getAppStandbyBucket()
      */
     public static final int STANDBY_BUCKET_RESTRICTED = 45;
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 772845d..8d65c92 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -84,6 +84,8 @@
 import java.util.Arrays;
 import java.util.Deque;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -189,6 +191,9 @@
     // Only show an annoying dialog at most every 30 seconds
     private static final long MIN_DIALOG_INTERVAL_MS = 30000;
 
+    // Only log a dropbox entry at most every 30 seconds
+    private static final long MIN_DROPBOX_INTERVAL_MS = 3000;
+
     // How many Span tags (e.g. animations) to report.
     private static final int MAX_SPAN_TAGS = 20;
 
@@ -1752,16 +1757,20 @@
             // Not perfect, but fast and good enough for dup suppression.
             Integer crashFingerprint = info.hashCode();
             long lastViolationTime = 0;
-            if (mLastViolationTime != null) {
-                Long vtime = mLastViolationTime.get(crashFingerprint);
-                if (vtime != null) {
-                    lastViolationTime = vtime;
-                }
-            } else {
-                mLastViolationTime = new ArrayMap<>(1);
-            }
             long now = SystemClock.uptimeMillis();
-            mLastViolationTime.put(crashFingerprint, now);
+            if (sLogger == LOGCAT_LOGGER) { // Don't throttle it if there is a non-default logger
+                if (mLastViolationTime != null) {
+                    Long vtime = mLastViolationTime.get(crashFingerprint);
+                    if (vtime != null) {
+                        lastViolationTime = vtime;
+                    }
+                    clampViolationTimeMap(mLastViolationTime, Math.max(MIN_LOG_INTERVAL_MS,
+                                Math.max(MIN_DIALOG_INTERVAL_MS, MIN_DROPBOX_INTERVAL_MS)));
+                } else {
+                    mLastViolationTime = new ArrayMap<>(1);
+                }
+                mLastViolationTime.put(crashFingerprint, now);
+            }
             long timeSinceLastViolationMillis =
                     lastViolationTime == 0 ? Long.MAX_VALUE : (now - lastViolationTime);
 
@@ -1780,7 +1789,8 @@
                 penaltyMask |= PENALTY_DIALOG;
             }
 
-            if (info.penaltyEnabled(PENALTY_DROPBOX) && lastViolationTime == 0) {
+            if (info.penaltyEnabled(PENALTY_DROPBOX)
+                    && timeSinceLastViolationMillis > MIN_DROPBOX_INTERVAL_MS) {
                 penaltyMask |= PENALTY_DROPBOX;
             }
 
@@ -2215,6 +2225,23 @@
     @UnsupportedAppUsage
     private static final HashMap<Integer, Long> sLastVmViolationTime = new HashMap<>();
 
+    /**
+     * Clamp the given map by removing elements with timestamp older than the given retainSince.
+     */
+    private static void clampViolationTimeMap(final @NonNull Map<Integer, Long> violationTime,
+            final long retainSince) {
+        final Iterator<Map.Entry<Integer, Long>> iterator = violationTime.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Map.Entry<Integer, Long> e = iterator.next();
+            if (e.getValue() < retainSince) {
+                // Remove stale entries
+                iterator.remove();
+            }
+        }
+        // Ideally we'd cap the total size of the map, though it'll involve quickselect of topK,
+        // seems not worth it (saving some space immediately but they will be obsoleted soon anyway)
+    }
+
     /** @hide */
     public static void onVmPolicyViolation(Violation originStack) {
         onVmPolicyViolation(originStack, false);
@@ -2238,13 +2265,17 @@
         final long now = SystemClock.uptimeMillis();
         long lastViolationTime;
         long timeSinceLastViolationMillis = Long.MAX_VALUE;
-        synchronized (sLastVmViolationTime) {
-            if (sLastVmViolationTime.containsKey(fingerprint)) {
-                lastViolationTime = sLastVmViolationTime.get(fingerprint);
-                timeSinceLastViolationMillis = now - lastViolationTime;
-            }
-            if (timeSinceLastViolationMillis > MIN_VM_INTERVAL_MS) {
-                sLastVmViolationTime.put(fingerprint, now);
+        if (sLogger == LOGCAT_LOGGER) { // Don't throttle it if there is a non-default logger
+            synchronized (sLastVmViolationTime) {
+                if (sLastVmViolationTime.containsKey(fingerprint)) {
+                    lastViolationTime = sLastVmViolationTime.get(fingerprint);
+                    timeSinceLastViolationMillis = now - lastViolationTime;
+                }
+                if (timeSinceLastViolationMillis > MIN_VM_INTERVAL_MS) {
+                    sLastVmViolationTime.put(fingerprint, now);
+                }
+                clampViolationTimeMap(sLastVmViolationTime,
+                        now - Math.max(MIN_VM_INTERVAL_MS, MIN_LOG_INTERVAL_MS));
             }
         }
         if (timeSinceLastViolationMillis <= MIN_VM_INTERVAL_MS) {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index a415dc5..2465b0e 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2699,15 +2699,12 @@
      * @param name     the user's name
      * @param userType the type of user, such as {@link UserManager#USER_TYPE_FULL_GUEST}.
      * @param flags    UserInfo flags that specify user properties.
-     * @return the {@link UserInfo} object for the created user,
-     *         or throws {@link UserOperationException} if the user could not be created
-     *         and calling app is targeting {@link android.os.Build.VERSION_CODES#R} or above
-     *         (otherwise returns {@code null}).
+     * @return the {@link UserInfo} object for the created user, or {@code null} if the user
+     *         could not be created.
      *
-     * @throws UserOperationException if the user could not be created and the calling app is
-     *         targeting {@link android.os.Build.VERSION_CODES#R} or above.
-     * @hide
      * @see UserInfo
+     *
+     * @hide
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
@@ -2716,8 +2713,7 @@
         try {
             return mService.createUserWithThrow(name, userType, flags);
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            return null;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -2743,25 +2739,19 @@
      * com.android.server.pm.UserManagerService#ALLOWED_FLAGS_FOR_CREATE_USERS_PERMISSION}.
      *
      * @param userType the type of user, such as {@link UserManager#USER_TYPE_FULL_GUEST}.
-     * @return the {@link UserInfo} object for the created user,
-     *         or throws {@link UserOperationException} if the user could not be created
-     *         and calling app is targeting {@link android.os.Build.VERSION_CODES#R} or above
-     *         (otherwise returns {@code null}).
+     * @return the {@link UserInfo} object for the created user.
      *
-     * @throws UserOperationException if the user could not be created and the calling app is
-     *         targeting {@link android.os.Build.VERSION_CODES#R} or above.
-     *
+     * @throws UserOperationException if the user could not be created.
      * @hide
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
-    public @Nullable UserInfo preCreateUser(@NonNull String userType)
+    public @NonNull UserInfo preCreateUser(@NonNull String userType)
             throws UserOperationException {
         try {
             return mService.preCreateUserWithThrow(userType);
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            throw UserOperationException.from(e);
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -2771,18 +2761,14 @@
      * Creates a guest user and configures it.
      * @param context an application context
      * @param name the name to set for the user
-     * @return the {@link UserInfo} object for the created user,
-     *         or throws {@link UserOperationException} if the user could not be created
-     *         and calling app is targeting {@link android.os.Build.VERSION_CODES#R} or above
-     *         (otherwise returns {@code null}).
+     * @return the {@link UserInfo} object for the created user, or {@code null} if the user
+     *         could not be created.
      *
-     * @throws UserOperationException if the user could not be created and the calling app is
-     *         targeting {@link android.os.Build.VERSION_CODES#R} or above.
      * @hide
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
-    public UserInfo createGuest(Context context, String name) throws UserOperationException {
+    public UserInfo createGuest(Context context, String name) {
         UserInfo guest = null;
         try {
             guest = mService.createUserWithThrow(name, USER_TYPE_FULL_GUEST, 0);
@@ -2791,8 +2777,7 @@
                         Settings.Secure.SKIP_FIRST_USE_HINTS, "1", guest.id);
             }
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            return null;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -2902,26 +2887,20 @@
      * @param userId new user will be a profile of this user.
      * @param disallowedPackages packages that will not be installed in the profile being created.
      *
-     * @return the {@link UserInfo} object for the created user,
-     *         or throws {@link UserOperationException} if the user could not be created
-     *         and calling app is targeting {@link android.os.Build.VERSION_CODES#R} or above
-     *         (otherwise returns {@code null}).
+     * @return the {@link UserInfo} object for the created user, or {@code null} if the user could
+     *         not be created.
      *
-     * @throws UserOperationException if the user could not be created and the calling app is
-     *         targeting {@link android.os.Build.VERSION_CODES#R} or above.
      * @hide
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
     public UserInfo createProfileForUser(String name, @NonNull String userType,
-            @UserInfoFlag int flags, @UserIdInt int userId, String[] disallowedPackages)
-            throws UserOperationException {
+            @UserInfoFlag int flags, @UserIdInt int userId, String[] disallowedPackages) {
         try {
             return mService.createProfileForUserWithThrow(name, userType, flags, userId,
                     disallowedPackages);
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            return null;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -2938,13 +2917,12 @@
             Manifest.permission.CREATE_USERS})
     public UserInfo createProfileForUserEvenWhenDisallowed(String name,
             @NonNull String userType, @UserInfoFlag int flags, @UserIdInt int userId,
-            String[] disallowedPackages) throws UserOperationException {
+            String[] disallowedPackages) {
         try {
             return mService.createProfileForUserEvenWhenDisallowedWithThrow(name, userType, flags,
                     userId, disallowedPackages);
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            return null;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -2955,18 +2933,14 @@
      * restrictions and adds shared accounts.
      *
      * @param name profile's name
-     * @return the {@link UserInfo} object for the created user,
-     *         or throws {@link UserOperationException} if the user could not be created
-     *         and calling app is targeting {@link android.os.Build.VERSION_CODES#R} or above
-     *         (otherwise returns {@code null}).
+     * @return the {@link UserInfo} object for the created user, or {@code null} if the user
+     *         could not be created.
      *
-     * @throws UserOperationException if the user could not be created and the calling app is
-     *         targeting {@link android.os.Build.VERSION_CODES#R} or above.
      * @hide
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
-    public UserInfo createRestrictedProfile(String name) throws UserOperationException {
+    public UserInfo createRestrictedProfile(String name) {
         try {
             UserHandle parentUserHandle = Process.myUserHandle();
             UserInfo user = mService.createRestrictedProfileWithThrow(name,
@@ -2977,8 +2951,7 @@
             }
             return user;
         } catch (ServiceSpecificException e) {
-            return returnNullOrThrowUserOperationException(e,
-                    mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R);
+            return null;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -4009,15 +3982,15 @@
      * Sets the user's photo.
      * @param userId the user for whom to change the photo.
      * @param icon the bitmap to set as the photo.
+     *
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
-    public void setUserIcon(@UserIdInt int userId, @NonNull Bitmap icon)
-            throws UserOperationException {
+    public void setUserIcon(@UserIdInt int userId, @NonNull Bitmap icon) {
         try {
             mService.setUserIcon(userId, icon);
         } catch (ServiceSpecificException e) {
-            throw UserOperationException.from(e);
+            return;
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -4027,6 +4000,10 @@
      * Sets the context user's photo.
      *
      * @param icon the bitmap to set as the photo.
+     *
+     * @throws UserOperationException according to the function signature, but may not actually
+     * throw it in practice. Catch RuntimeException instead.
+     *
      * @hide
      */
     @SystemApi
diff --git a/core/java/android/os/strictmode/Violation.java b/core/java/android/os/strictmode/Violation.java
index 31c7d58..0edb78a 100644
--- a/core/java/android/os/strictmode/Violation.java
+++ b/core/java/android/os/strictmode/Violation.java
@@ -18,7 +18,58 @@
 
 /** Root class for all StrictMode violations. */
 public abstract class Violation extends Throwable {
+    private int mHashCode;
+    private boolean mHashCodeValid;
+
     Violation(String message) {
         super(message);
     }
+
+    @Override
+    public int hashCode() {
+        synchronized (this) {
+            if (mHashCodeValid) {
+                return mHashCode;
+            }
+            final String message = getMessage();
+            final Throwable cause = getCause();
+            int hashCode = message != null ? message.hashCode() : getClass().hashCode();
+            hashCode = hashCode * 37 + calcStackTraceHashCode(getStackTrace());
+            hashCode = hashCode * 37 + (cause != null ? cause.toString().hashCode() : 0);
+            mHashCodeValid = true;
+            return mHashCode = hashCode;
+        }
+    }
+
+    @Override
+    public synchronized Throwable initCause(Throwable cause) {
+        mHashCodeValid = false;
+        return super.initCause(cause);
+    }
+
+    @Override
+    public void setStackTrace(StackTraceElement[] stackTrace) {
+        super.setStackTrace(stackTrace);
+        synchronized (this) {
+            mHashCodeValid = false;
+        }
+    }
+
+    @Override
+    public synchronized Throwable fillInStackTrace() {
+        mHashCodeValid = false;
+        return super.fillInStackTrace();
+    }
+
+    private static int calcStackTraceHashCode(final StackTraceElement[] stackTrace) {
+        int hashCode = 17;
+        if (stackTrace != null) {
+            for (int i = 0; i < stackTrace.length; i++) {
+                if (stackTrace[i] != null) {
+                    hashCode = hashCode * 37 + stackTrace[i].hashCode();
+                }
+            }
+        }
+        return hashCode;
+    }
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4abb2c5..64d9c9d 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12009,7 +12009,7 @@
          * @see #ENABLE_RESTRICTED_BUCKET
          * @hide
          */
-        public static final int DEFAULT_ENABLE_RESTRICTED_BUCKET = 1;
+        public static final int DEFAULT_ENABLE_RESTRICTED_BUCKET = 0;
 
         /**
          * Whether or not app auto restriction is enabled. When it is enabled, settings app will
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ed40483..b860bac 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -21,7 +21,6 @@
 import static android.view.InputDevice.SOURCE_CLASS_NONE;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
-import static android.view.InsetsState.LAST_TYPE;
 import static android.view.InsetsState.SIZE;
 import static android.view.View.PFLAG_DRAW_ANIMATION;
 import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
@@ -4617,6 +4616,9 @@
     }
 
     void dispatchDetachedFromWindow() {
+        // Make sure we free-up insets resources if view never received onWindowFocusLost()
+        // because of a die-signal
+        mInsetsController.onWindowFocusLost();
         mFirstInputStage.onDetachedFromWindow();
         if (mView != null && mView.mAttachInfo != null) {
             mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);
diff --git a/core/res/res/drawable/chooser_action_button_bg.xml b/core/res/res/drawable/chooser_action_button_bg.xml
index a434c0b..0dd9e9c7 100644
--- a/core/res/res/drawable/chooser_action_button_bg.xml
+++ b/core/res/res/drawable/chooser_action_button_bg.xml
@@ -25,8 +25,8 @@
             <shape android:shape="rectangle">
               <corners android:radius="16dp"></corners>
                 <stroke android:width="1dp"
-                        android:color="?attr/textColorSecondary" />
-                <solid android:color="?attr/colorBackground" />
+                        android:color="?attr/opacityListDivider" />
+                <solid android:color="?attr/colorBackgroundFloating" />
             </shape>
         </inset>
     </item>
diff --git a/core/res/res/layout/chooser_action_button.xml b/core/res/res/layout/chooser_action_button.xml
index 119b2e9..6af7937 100644
--- a/core/res/res/layout/chooser_action_button.xml
+++ b/core/res/res/layout/chooser_action_button.xml
@@ -19,12 +19,12 @@
     android:paddingStart="12dp"
     android:paddingEnd="12dp"
     android:drawablePadding="8dp"
-    android:textColor="?android:textColorSecondary"
+    android:textColor="?android:textColorPrimary"
     android:textSize="12sp"
     android:maxWidth="192dp"
     android:singleLine="true"
     android:clickable="true"
     android:background="@drawable/chooser_action_button_bg"
-    android:drawableTint="?android:attr/colorControlNormal"
+    android:drawableTint="@color/chooser_chip_icon"
     android:drawableTintMode="src_in"
     />
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index c413f8b..1242c6d 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -230,6 +230,7 @@
     <color name="resolver_text_color_secondary_dark">#ffC4C6C6</color>
     <color name="resolver_empty_state_text">#FF202124</color>
     <color name="resolver_empty_state_icon">#FF5F6368</color>
+    <color name="chooser_chip_icon">#FF1A73E8</color> <!-- Blue 600 -->
 
     <!-- Color for personal app suspension notification button text and icon tint. -->
     <color name="personal_apps_suspension_notification_color">#1A73E8</color>
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index eb6ee95..5f231ff 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -157,7 +157,7 @@
   table_value->dataType = entry->type;
   table_value->data = entry->value;
 
-  return Result(ResTable_entry_handle::managed(table_entry));
+  return Result(ResTable_entry_handle::managed(table_entry, [](auto p) { free(p); }));
 }
 
 static bool is_word_aligned(const void* data) {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 21be81c..e351a46 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1601,8 +1601,8 @@
       entry_ = handle.entry_;
     }
 
-    inline static ResTable_entry_handle managed(ResTable_entry* entry)  {
-      return ResTable_entry_handle(std::shared_ptr<const ResTable_entry>(entry));
+    inline static ResTable_entry_handle managed(ResTable_entry* entry, void (*deleter)(void *)) {
+      return ResTable_entry_handle(std::shared_ptr<const ResTable_entry>(entry, deleter));
     }
 
     inline static ResTable_entry_handle unmanaged(const ResTable_entry* entry)  {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
index bcff634..9d52098 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
@@ -30,7 +30,7 @@
  */
 @ProvidesInterface(version = FalsingManager.VERSION)
 public interface FalsingManager {
-    int VERSION = 3;
+    int VERSION = 4;
 
     void onSuccessfulUnlock();
 
@@ -88,11 +88,11 @@
 
     void onScreenOff();
 
-    void onNotificatonStopDismissing();
+    void onNotificationStopDismissing();
 
     void onNotificationDismissed();
 
-    void onNotificatonStartDismissing();
+    void onNotificationStartDismissing();
 
     void onNotificationDoubleTap(boolean accepted, float dx, float dy);
 
diff --git a/packages/SystemUI/res-keyguard/drawable/qs_media_seamless_background.xml b/packages/SystemUI/res-keyguard/drawable/qs_media_seamless_background.xml
new file mode 100644
index 0000000..3790378
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/qs_media_seamless_background.xml
@@ -0,0 +1,26 @@
+<?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
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/media_seamless_border">
+    <item android:id="@android:id/background">
+        <shape
+            android:color="@android:color/transparent">
+            <stroke android:width="1dp" android:color="@color/media_seamless_border"/>
+            <corners android:radius="24dp"/>
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/qs_media_divider.xml b/packages/SystemUI/res-keyguard/layout/qs_media_divider.xml
new file mode 100644
index 0000000..1be489c
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/qs_media_divider.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="1dp"
+    android:layout_marginBottom="16dp"
+    android:background="@color/media_divider">
+</View>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index dc91731..ee1173b 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -23,7 +23,7 @@
     android:clipChildren="false"
     android:clipToPadding="false"
     >
-    <com.android.systemui.media.UnboundHorizontalScrollView
+    <com.android.systemui.media.MediaScrollView
         android:id="@+id/media_carousel_scroller"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -41,14 +41,12 @@
             >
             <!-- QSMediaPlayers will be added here dynamically -->
         </LinearLayout>
-    </com.android.systemui.media.UnboundHorizontalScrollView>
+    </com.android.systemui.media.MediaScrollView>
     <com.android.systemui.qs.PageIndicator
         android:id="@+id/media_page_indicator"
         android:layout_width="wrap_content"
         android:layout_height="48dp"
         android:layout_marginBottom="4dp"
-        android:layout_gravity="center_horizontal|bottom"
-        android:gravity="center"
         android:tint="@color/media_primary_text"
     />
 </FrameLayout>
diff --git a/packages/SystemUI/res/layout/media_carousel_settings_button.xml b/packages/SystemUI/res/layout/media_carousel_settings_button.xml
new file mode 100644
index 0000000..4570cb1
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_carousel_settings_button.xml
@@ -0,0 +1,29 @@
+<?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.
+  -->
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/settings_cog"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:contentDescription="@string/controls_media_settings_button"
+    android:paddingStart="30dp"
+    android:paddingEnd="30dp"
+    android:paddingBottom="20dp"
+    android:paddingTop="20dp"
+    android:src="@drawable/ic_settings"
+    android:tint="@color/notification_gear_color"
+    android:visibility="invisible"
+    android:forceHasOverlappingRendering="false"/>
diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml
index cc7ed19..07bbb8f 100644
--- a/packages/SystemUI/res/layout/media_view.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -26,6 +26,7 @@
     android:gravity="center_horizontal|fill_vertical"
     android:background="@drawable/qs_media_background">
 
+    <!-- As per Material Design on Biderectionality, this is forced to LTR in code -->
     <FrameLayout
         android:id="@+id/notification_media_progress_time"
         android:layout_width="0dp"
@@ -36,7 +37,7 @@
             android:id="@+id/media_elapsed_time"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
+            android:layout_alignParentStart="true"
             android:fontFamily="@*android:string/config_bodyFontFamily"
             android:textColor="@color/media_primary_text"
             android:gravity="start"
@@ -46,13 +47,36 @@
             android:id="@+id/media_total_time"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_alignParentRight="true"
+            android:layout_alignParentEnd="true"
             android:fontFamily="@*android:string/config_bodyFontFamily"
             android:textColor="@color/media_primary_text"
             android:gravity="end"
             android:textSize="14sp" />
     </FrameLayout>
 
+    <!--  Actions must be ordered left-to-right even in RTL layout.  However, they appear in a chain
+    with the album art and the title, and must as a group appear at the end of that chain.  This is
+    accomplished by having the guidebox (an invisible view that is positioned around all 5 actions)
+    in the chain with the album art and the title.  The actions are in a LTR chain bounded by that
+    guidebox, and the ambiguity of how wide the guidebox should be is resolved by using a barrier
+    which forces it's starting edge to be as far to the end as possible while fitting the actions.
+    -->
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/media_action_barrier"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:orientation="vertical"
+        app:barrierDirection="start"
+        />
+
+    <View
+        android:id="@+id/media_action_guidebox"
+        android:layout_width="0dp"
+        android:layout_height="48dp"
+        android:layout_marginTop="16dp"
+        android:visibility="invisible"
+        />
+
     <ImageButton
         android:id="@+id/action0"
         style="@style/MediaPlayer.Button"
@@ -94,20 +118,20 @@
         android:id="@+id/media_seamless"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:foreground="@*android:drawable/media_seamless_background"
+        android:foreground="@drawable/qs_media_seamless_background"
         android:background="@drawable/qs_media_light_source"
         android:orientation="horizontal"
         android:forceHasOverlappingRendering="false"
-        android:paddingLeft="12dp"
+        android:paddingStart="12dp"
         android:paddingTop="6dp"
-        android:paddingRight="12dp"
+        android:paddingEnd="12dp"
         android:paddingBottom="6dp">
 
         <ImageView
             android:id="@+id/media_seamless_image"
             android:layout_width="@dimen/qs_seamless_icon_size"
             android:layout_height="@dimen/qs_seamless_icon_size"
-            android:layout_marginRight="8dp"
+            android:layout_marginEnd="8dp"
             android:tint="@color/media_primary_text"
             android:src="@*android:drawable/ic_media_seamless" />
 
@@ -115,10 +139,11 @@
             android:id="@+id/media_seamless_text"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:fontFamily="@*android:string/config_bodyFontFamily"
+            android:fontFamily="@*android:string/config_headlineFontFamily"
             android:singleLine="true"
             android:text="@*android:string/ext_media_seamless_action"
             android:textColor="@color/media_primary_text"
+            android:textDirection="locale"
             android:textSize="14sp" />
     </LinearLayout>
 
@@ -140,6 +165,7 @@
         />
 
     <!-- Seek Bar -->
+    <!-- As per Material Design on Biderectionality, this is forced to LTR in code -->
     <SeekBar
         android:id="@+id/media_progress_bar"
         style="@android:style/Widget.ProgressBar.Horizontal"
@@ -161,6 +187,8 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:singleLine="true"
+        android:fontFamily="@*android:string/config_headlineFontFamily"
+        android:textDirection="locale"
         android:textSize="14sp" />
 
     <!-- Song name -->
@@ -171,6 +199,7 @@
         android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
         android:singleLine="true"
         android:textColor="@color/media_primary_text"
+        android:textDirection="locale"
         android:textSize="16sp" />
 
     <!-- Artist name -->
@@ -178,16 +207,17 @@
         android:id="@+id/header_artist"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:fontFamily="@*android:string/config_bodyFontFamily"
+        android:fontFamily="@*android:string/config_headlineFontFamily"
         android:singleLine="true"
         android:textColor="@color/media_secondary_text"
+        android:textDirection="locale"
         android:textSize="14sp" />
 
     <com.android.internal.widget.CachingIconView
         android:id="@+id/icon"
         android:tint="@color/media_primary_text"
-        android:layout_width="16dp"
-        android:layout_height="16dp" />
+        android:layout_width="20dp"
+        android:layout_height="20dp" />
 
     <!-- Buttons to remove this view when no longer needed -->
     <include
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index 761ab03..597644b 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -62,6 +62,8 @@
             android:focusable="true"
             android:accessibilityTraversalBefore="@android:id/edit">
             <include layout="@layout/qs_footer_impl" />
+            <include layout="@layout/qs_media_divider"
+                android:id="@+id/divider"/>
         </com.android.systemui.qs.QSPanel>
     </com.android.systemui.qs.NonInterceptingScrollView>
 
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 196357c..cb9e178 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -81,6 +81,8 @@
     <color name="global_screenshot_dismiss_foreground">#FFFFFF</color>
     <color name="global_screenshot_background_protection_start">#80000000</color> <!-- 50% black -->
 
+    <!-- Media -->
+    <color name="media_divider">#85ffffff</color>
 
     <!-- Biometric dialog colors -->
     <color name="biometric_dialog_gray">#ff888888</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index d8a3c8c..994a181 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -248,6 +248,8 @@
     <color name="media_secondary_text">#99ffffff</color> <!-- 60% -->
     <color name="media_seekbar_progress">#c0ffffff</color>
     <color name="media_disabled">#80ffffff</color>
+    <color name="media_seamless_border">#26ffffff</color> <!-- 15% -->
+    <color name="media_divider">#1d000000</color>
 
     <!-- controls -->
     <color name="control_primary_text">#E6FFFFFF</color>
diff --git a/packages/SystemUI/res/xml/media_collapsed.xml b/packages/SystemUI/res/xml/media_collapsed.xml
index 0e886d6..811e0e3 100644
--- a/packages/SystemUI/res/xml/media_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_collapsed.xml
@@ -85,7 +85,7 @@
         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_constraintEnd_toStartOf="@id/media_action_barrier"
         app:layout_constraintHorizontal_bias="0"/>
 
     <!-- Artist name -->
@@ -97,7 +97,7 @@
         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_constraintEnd_toStartOf="@id/media_action_barrier"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintHorizontal_bias="0"/>
 
@@ -128,15 +128,37 @@
         />
 
     <Constraint
+        android:id="@+id/media_action_barrier"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:orientation="vertical"
+        app:layout_constraintTop_toTopOf="parent"
+        app:barrierDirection="start"
+        app:constraint_referenced_ids="media_action_guidebox,action0,action1,action2,action3,action4"
+        />
+
+    <Constraint
+        android:id="@+id/media_action_guidebox"
+        android:layout_width="0dp"
+        android:layout_height="48dp"
+        android:layout_marginTop="18dp"
+        android:visibility="invisible"
+        app:layout_constraintTop_toBottomOf="@id/app_name"
+        app:layout_constraintStart_toEndOf="@id/header_title"
+        app:layout_constraintEnd_toEndOf="parent"
+        />
+
+    <Constraint
         android:id="@+id/action0"
         android:layout_width="48dp"
         android:layout_height="48dp"
         android:layout_marginStart="4dp"
-        android:layout_marginTop="16dp"
+        android:layout_marginEnd="4dp"
+        android:layout_marginTop="18dp"
         android:visibility="gone"
         app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintTop_toBottomOf="@id/app_name"
-        app:layout_constraintLeft_toRightOf="@id/header_title"
+        app:layout_constraintLeft_toLeftOf="@id/media_action_guidebox"
         app:layout_constraintRight_toLeftOf="@id/action1"
         >
     </Constraint>
@@ -188,9 +210,10 @@
         android:layout_marginEnd="4dp"
         android:visibility="gone"
         android:layout_marginTop="18dp"
+        app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintTop_toBottomOf="@id/app_name"
         app:layout_constraintLeft_toRightOf="@id/action3"
-        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintRight_toRightOf="@id/media_action_guidebox"
         >
     </Constraint>
 </ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_expanded.xml b/packages/SystemUI/res/xml/media_expanded.xml
index 9b4caa4..8432abc 100644
--- a/packages/SystemUI/res/xml/media_expanded.xml
+++ b/packages/SystemUI/res/xml/media_expanded.xml
@@ -182,6 +182,7 @@
         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_toRightOf="@id/action3"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="@id/action0"
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index c985b08..50828e8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -1504,6 +1504,7 @@
                     && ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
                 mBubbleContainer.removeViewAt(i);
                 bubble.cleanupViews();
+                updatePointerPosition();
                 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
                 return;
             }
@@ -1554,6 +1555,14 @@
             mBubbleData.setShowingOverflow(true);
         }
 
+        if (mIsExpanded && mIsExpansionAnimating) {
+            // If the bubble selection changed during the expansion animation, the expanding bubble
+            // probably crashed or immediately removed itself (or, we just got unlucky with a new
+            // auto-expanding bubble showing up at just the right time). Cancel the animations so we
+            // can start fresh.
+            cancelAllExpandCollapseSwitchAnimations();
+        }
+
         // If we're expanded, screenshot the currently expanded bubble (before expanding the newly
         // selected bubble) so we can animate it out.
         if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
@@ -1879,35 +1888,37 @@
             mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
         }
 
-        mDelayedAnimationHandler.postDelayed(() ->
-                        PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
-                                .spring(AnimatableScaleMatrix.SCALE_X,
-                                        AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
-                                        mScaleInSpringConfig)
-                                .spring(AnimatableScaleMatrix.SCALE_Y,
-                                        AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
-                                        mScaleInSpringConfig)
-                                .addUpdateListener((target, values) -> {
-                                    if (mExpandedBubble.getIconView() == null) {
-                                        return;
-                                    }
-                                    mExpandedViewContainerMatrix.postTranslate(
-                                            mExpandedBubble.getIconView().getTranslationX()
-                                                    - bubbleWillBeAtX,
-                                            0);
-                                    mExpandedViewContainer.setAnimationMatrix(
-                                            mExpandedViewContainerMatrix);
-                                })
-                                .withEndActions(() -> {
-                                    if (mExpandedBubble != null
-                                            && mExpandedBubble.getExpandedView() != null) {
-                                        mExpandedBubble.getExpandedView()
-                                                .setContentVisibility(true);
-                                        mExpandedBubble.getExpandedView()
-                                                .setSurfaceZOrderedOnTop(false);
-                                    }
-                                })
-                                .start(), startDelay);
+        mDelayedAnimationHandler.postDelayed(() -> {
+            PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+            PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+                    .spring(AnimatableScaleMatrix.SCALE_X,
+                            AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+                            mScaleInSpringConfig)
+                    .spring(AnimatableScaleMatrix.SCALE_Y,
+                            AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+                            mScaleInSpringConfig)
+                    .addUpdateListener((target, values) -> {
+                        if (mExpandedBubble.getIconView() == null) {
+                            return;
+                        }
+                        mExpandedViewContainerMatrix.postTranslate(
+                                mExpandedBubble.getIconView().getTranslationX()
+                                        - bubbleWillBeAtX,
+                                0);
+                        mExpandedViewContainer.setAnimationMatrix(
+                                mExpandedViewContainerMatrix);
+                    })
+                    .withEndActions(() -> {
+                        if (mExpandedBubble != null
+                                && mExpandedBubble.getExpandedView() != null) {
+                            mExpandedBubble.getExpandedView()
+                                    .setContentVisibility(true);
+                            mExpandedBubble.getExpandedView()
+                                    .setSurfaceZOrderedOnTop(false);
+                        }
+                    })
+                    .start();
+        }, startDelay);
     }
 
     private void animateCollapse() {
@@ -2035,6 +2046,7 @@
                 return;
             }
 
+            PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
             PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
                     .spring(AnimatableScaleMatrix.SCALE_X,
                             AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
@@ -2068,6 +2080,15 @@
         mIsBubbleSwitchAnimating = false;
     }
 
+    private void cancelAllExpandCollapseSwitchAnimations() {
+        cancelDelayedExpandCollapseSwitchAnimations();
+
+        PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel();
+        PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+
+        mExpandedViewContainer.setAnimationMatrix(null);
+    }
+
     private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
         if (mExpandListener != null && bubble != null) {
             mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
@@ -2587,6 +2608,7 @@
             bev.setContentVisibility(false);
             mExpandedViewContainerMatrix.setScaleX(0f);
             mExpandedViewContainerMatrix.setScaleY(0f);
+            mExpandedViewContainerMatrix.setTranslate(0f, 0f);
             mExpandedViewContainer.setVisibility(View.INVISIBLE);
             mExpandedViewContainer.setAlpha(0f);
             mExpandedViewContainer.addView(bev);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 8e23252..f2a4f15 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -237,6 +237,10 @@
                 }
 
                 mAfterExpand = null;
+
+                // Update bubble positions in case any bubbles were added or removed during the
+                // expansion animation.
+                updateBubblePositions();
             };
         } else {
             after = () -> {
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
index e105795a..646e620 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java
@@ -201,7 +201,7 @@
     }
 
     @Override
-    public void onNotificatonStopDismissing() {
+    public void onNotificationStopDismissing() {
 
     }
 
@@ -211,7 +211,7 @@
     }
 
     @Override
-    public void onNotificatonStartDismissing() {
+    public void onNotificationStartDismissing() {
 
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
index 37c7a2e..cc64fb5 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java
@@ -481,15 +481,15 @@
         mDataCollector.onNotificationDismissed();
     }
 
-    public void onNotificatonStartDismissing() {
+    public void onNotificationStartDismissing() {
         if (FalsingLog.ENABLED) {
-            FalsingLog.i("onNotificatonStartDismissing", "");
+            FalsingLog.i("onNotificationStartDismissing", "");
         }
         mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
         mDataCollector.onNotificatonStartDismissing();
     }
 
-    public void onNotificatonStopDismissing() {
+    public void onNotificationStopDismissing() {
         mDataCollector.onNotificatonStopDismissing();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
index 79b691b..ef2ef45 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java
@@ -302,8 +302,8 @@
     }
 
     @Override
-    public void onNotificatonStopDismissing() {
-        mInternalFalsingManager.onNotificatonStopDismissing();
+    public void onNotificationStopDismissing() {
+        mInternalFalsingManager.onNotificationStopDismissing();
     }
 
     @Override
@@ -312,8 +312,8 @@
     }
 
     @Override
-    public void onNotificatonStartDismissing() {
-        mInternalFalsingManager.onNotificatonStartDismissing();
+    public void onNotificationStartDismissing() {
+        mInternalFalsingManager.onNotificationStartDismissing();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
index caab187..62254a6 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java
@@ -380,7 +380,7 @@
 
 
     @Override
-    public void onNotificatonStopDismissing() {
+    public void onNotificationStopDismissing() {
     }
 
     @Override
@@ -388,7 +388,7 @@
     }
 
     @Override
-    public void onNotificatonStartDismissing() {
+    public void onNotificationStartDismissing() {
         updateInteractionType(Classifier.NOTIFICATION_DISMISS);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 5f43e43..8ef20f8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -46,7 +46,9 @@
         })
     }
 
-    private var view: MediaHeaderView? = null
+    var visibilityChangedListener: ((Boolean) -> Unit)? = null
+    var view: MediaHeaderView? = null
+        private set
 
     /**
      * Attach this controller to a media view, initializing its state
@@ -54,9 +56,10 @@
     fun attach(mediaView: MediaHeaderView) {
         view = mediaView
         // First let's set the desired state that we want for this host
-        mediaHost.visibleChangedListener = { updateVisibility() }
+        mediaHost.addVisibilityChangeListener { updateVisibility() }
         mediaHost.expansion = 0.0f
         mediaHost.showsOnlyActiveMedia = true
+        mediaHost.falsingProtectionNeeded = true
 
         // Let's now initialize this view, which also creates the host view for us.
         mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -70,6 +73,11 @@
                 !bypassController.bypassEnabled &&
                 keyguardOrUserSwitcher &&
                 notifLockscreenUserManager.shouldShowLockscreenNotifications()
-        view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE
+        val previousVisibility = view?.visibility ?: View.GONE
+        val newVisibility = if (shouldBeVisible) View.VISIBLE else View.GONE
+        view?.visibility = newVisibility
+        if (previousVisibility != newVisibility) {
+            visibilityChangedListener?.invoke(shouldBeVisible)
+        }
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
similarity index 65%
rename from packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
rename to packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index bccc3ab..db45a5f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -1,42 +1,66 @@
 package com.android.systemui.media
 
 import android.content.Context
+import android.content.Intent
 import android.graphics.Color
+import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
 import android.view.LayoutInflater
-import android.view.GestureDetector
-import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
-import android.widget.HorizontalScrollView
 import android.widget.LinearLayout
-import androidx.core.view.GestureDetectorCompat
 import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.statusbar.notification.VisualStabilityManager
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.animation.requiresRemeasuring
+import com.android.systemui.util.concurrency.DelayableExecutor
 import javax.inject.Inject
 import javax.inject.Provider
 import javax.inject.Singleton
 
-private const val FLING_SLOP = 1000000
+private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
 
 /**
  * Class that is responsible for keeping the view carousel up to date.
  * This also handles changes in state and applies them to the media carousel like the expansion.
  */
 @Singleton
-class MediaViewManager @Inject constructor(
+class MediaCarouselController @Inject constructor(
     private val context: Context,
     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
     private val visualStabilityManager: VisualStabilityManager,
     private val mediaHostStatesManager: MediaHostStatesManager,
+    private val activityStarter: ActivityStarter,
+    @Main executor: DelayableExecutor,
     mediaManager: MediaDataCombineLatest,
-    configurationController: ConfigurationController
+    configurationController: ConfigurationController,
+    mediaDataManager: MediaDataManager,
+    falsingManager: FalsingManager
 ) {
+    /**
+     * The current width of the carousel
+     */
+    private var currentCarouselWidth: Int = 0
 
     /**
+     * The current height of the carousel
+     */
+    private var currentCarouselHeight: Int = 0
+
+    /**
+     * Are we currently showing only active players
+     */
+    private var currentlyShowingOnlyActive: Boolean = false
+
+    /**
+     * Is the player currently visible (at the end of the transformation
+     */
+    private var playersVisible: Boolean = false
+    /**
      * 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.
      */
@@ -73,17 +97,16 @@
     private var carouselMeasureHeight: Int = 0
     private var playerWidthPlusPadding: Int = 0
     private var desiredHostState: MediaHostState? = null
-    private val mediaCarousel: HorizontalScrollView
+    private val mediaCarousel: MediaScrollView
+    private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
     val mediaFrame: ViewGroup
     val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
+    private lateinit var settingsButton: View
     private val mediaData: MutableMap<String, MediaData> = mutableMapOf()
     private val mediaContent: ViewGroup
     private val pageIndicator: PageIndicator
-    private val gestureDetector: GestureDetectorCompat
     private val visualStabilityCallback: VisualStabilityManager.Callback
-    private var activeMediaIndex: Int = 0
     private var needsReordering: Boolean = false
-    private var scrollIntoCurrentMedia: Int = 0
     private var currentlyExpanded = true
         set(value) {
             if (field != value) {
@@ -93,50 +116,25 @@
                 }
             }
         }
-    private val scrollChangedListener = object : View.OnScrollChangeListener {
-        override fun onScrollChange(
-            v: View?,
-            scrollX: Int,
-            scrollY: Int,
-            oldScrollX: Int,
-            oldScrollY: Int
-        ) {
-            if (playerWidthPlusPadding == 0) {
-                return
-            }
-            onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
-                    scrollX % playerWidthPlusPadding)
-        }
-    }
-    private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
-        override fun onFling(
-            eStart: MotionEvent?,
-            eCurrent: MotionEvent?,
-            vX: Float,
-            vY: Float
-        ): Boolean {
-            return this@MediaViewManager.onFling(eStart, eCurrent, vX, vY)
-        }
-    }
-    private val touchListener = object : View.OnTouchListener {
-        override fun onTouch(view: View, motionEvent: MotionEvent?): Boolean {
-            return this@MediaViewManager.onTouch(view, motionEvent)
-        }
-    }
     private val configListener = object : ConfigurationController.ConfigurationListener {
         override fun onDensityOrFontScaleChanged() {
             recreatePlayers()
+            inflateSettingsButton()
+        }
+
+        override fun onOverlayChanged() {
+            inflateSettingsButton()
         }
     }
 
     init {
-        gestureDetector = GestureDetectorCompat(context, gestureListener)
         mediaFrame = inflateMediaCarousel()
         mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
         pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
-        mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
-        mediaCarousel.setOnTouchListener(touchListener)
-        mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER)
+        mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
+                executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
+                falsingManager)
+        inflateSettingsButton()
         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
         configurationController.addCallback(configListener)
         visualStabilityCallback = VisualStabilityManager.Callback {
@@ -161,6 +159,11 @@
                 removePlayer(key)
             }
         })
+        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+            // The pageIndicator is not laid out yet when we get the current state update,
+            // Lets make sure we have the right dimensions
+            updatePageIndicatorLocation()
+        }
         mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
             override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
                 if (location == desiredLocation) {
@@ -170,6 +173,20 @@
         })
     }
 
+    private fun inflateSettingsButton() {
+        val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
+                mediaFrame, false) as View
+        if (this::settingsButton.isInitialized) {
+            mediaFrame.removeView(settingsButton)
+        }
+        settingsButton = settings
+        mediaFrame.addView(settingsButton)
+        mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
+        settingsButton.setOnClickListener {
+            activityStarter.startActivity(settingsIntent, true /* dismissShade */)
+        }
+    }
+
     private fun inflateMediaCarousel(): ViewGroup {
         return LayoutInflater.from(context).inflate(R.layout.media_carousel,
                 UniqueObjectHostView(context), false) as ViewGroup
@@ -183,68 +200,7 @@
                 mediaContent.addView(view, 0)
             }
         }
-        updateMediaPaddings()
-        updatePlayerVisibilities()
-    }
-
-    private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
-        val wasScrolledIn = scrollIntoCurrentMedia != 0
-        scrollIntoCurrentMedia = scrollInAmount
-        val nowScrolledIn = scrollIntoCurrentMedia != 0
-        if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
-            activeMediaIndex = newIndex
-            updatePlayerVisibilities()
-        }
-        val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
-                scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
-        pageIndicator.setLocation(location)
-    }
-
-    private fun onTouch(view: View, motionEvent: MotionEvent?): Boolean {
-        if (gestureDetector.onTouchEvent(motionEvent)) {
-            return true
-        }
-        if (motionEvent?.getAction() == MotionEvent.ACTION_UP) {
-            val pos = mediaCarousel.scrollX % playerWidthPlusPadding
-            if (pos > playerWidthPlusPadding / 2) {
-                mediaCarousel.smoothScrollBy(playerWidthPlusPadding - pos, 0)
-            } else {
-                mediaCarousel.smoothScrollBy(-1 * pos, 0)
-            }
-            return true
-        }
-        return view.onTouchEvent(motionEvent)
-    }
-
-    private fun onFling(
-        eStart: MotionEvent?,
-        eCurrent: MotionEvent?,
-        vX: Float,
-        vY: Float
-    ): Boolean {
-        if (vX * vX < 0.5 * vY * vY) {
-            return false
-        }
-        if (vX * vX < FLING_SLOP) {
-            return false
-        }
-        val pos = mediaCarousel.scrollX
-        val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
-        var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex
-        destIndex = Math.max(0, destIndex)
-        destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
-        val view = mediaContent.getChildAt(destIndex)
-        mediaCarousel.smoothScrollTo(view.left, mediaCarousel.scrollY)
-        return true
-    }
-
-    private fun updatePlayerVisibilities() {
-        val scrolledIn = scrollIntoCurrentMedia != 0
-        for (i in 0 until mediaContent.childCount) {
-            val view = mediaContent.getChildAt(i)
-            val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
-            view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
-        }
+        mediaCarouselScrollHandler.onPlayersChanged()
     }
 
     private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
@@ -259,6 +215,7 @@
             existingPlayer = mediaControlPanelFactory.get()
             existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
                     mediaContent))
+            existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
             mediaPlayers[key] = existingPlayer
             val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                     ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -280,28 +237,18 @@
             }
         }
         existingPlayer?.bind(data)
-        updateMediaPaddings()
         updatePageIndicator()
-        updatePlayerVisibilities()
+        mediaCarouselScrollHandler.onPlayersChanged()
         mediaCarousel.requiresRemeasuring = true
     }
 
     private fun removePlayer(key: String) {
         val removed = mediaPlayers.remove(key)
         removed?.apply {
-            val beforeActive = mediaContent.indexOfChild(removed.view?.player) <=
-                    activeMediaIndex
+            mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
             mediaContent.removeView(removed.view?.player)
             removed.onDestroy()
-            updateMediaPaddings()
-            if (beforeActive) {
-                // also update the index here since the scroll below might not always lead
-                // to a scrolling changed
-                activeMediaIndex = Math.max(0, activeMediaIndex - 1)
-                mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX -
-                        playerWidthPlusPadding, 0)
-            }
-            updatePlayerVisibilities()
+            mediaCarouselScrollHandler.onPlayersChanged()
             updatePageIndicator()
         }
     }
@@ -317,20 +264,6 @@
         }
     }
 
-    private fun updateMediaPaddings() {
-        val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
-        val childCount = mediaContent.childCount
-        for (i in 0 until childCount) {
-            val mediaView = mediaContent.getChildAt(i)
-            val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
-            val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
-            if (layoutParams.marginEnd != desiredPaddingEnd) {
-                layoutParams.marginEnd = desiredPaddingEnd
-                mediaView.layoutParams = layoutParams
-            }
-        }
-    }
-
     private fun updatePageIndicator() {
         val numPages = mediaContent.getChildCount()
         pageIndicator.setNumPages(numPages, Color.WHITE)
@@ -342,6 +275,12 @@
     /**
      * 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.
+     *
+     * @param startLocation the start location of our state or -1 if this is directly set
+     * @param endLocation the ending location of our state.
+     * @param progress the progress of the transition between startLocation and endlocation. If
+     *                 this is not a guided transformation, this will be 1.0f
+     * @param immediately should this state be applied immediately, canceling all animations?
      */
     fun setCurrentState(
         @MediaLocation startLocation: Int,
@@ -349,9 +288,6 @@
         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 (progress == 1f || progress == 0f) 1f else 0f
         if (startLocation != currentStartLocation ||
                 endLocation != currentEndLocation ||
                 progress != currentTransitionProgress ||
@@ -363,6 +299,51 @@
             for (mediaPlayer in mediaPlayers.values) {
                 updatePlayerToState(mediaPlayer, immediately)
             }
+            maybeResetSettingsCog()
+        }
+    }
+
+    private fun updatePageIndicatorLocation() {
+        // Update the location of the page indicator, carousel clipping
+        pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f +
+                mediaCarouselScrollHandler.contentTranslation
+        val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
+        pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
+                layoutParams.bottomMargin).toFloat()
+    }
+
+    /**
+     * Update the dimension of this carousel.
+     */
+    private fun updateCarouselDimensions() {
+        var width = 0
+        var height = 0
+        for (mediaPlayer in mediaPlayers.values) {
+            val controller = mediaPlayer.mediaViewController
+            width = Math.max(width, controller.currentWidth)
+            height = Math.max(height, controller.currentHeight)
+        }
+        if (width != currentCarouselWidth || height != currentCarouselHeight) {
+            currentCarouselWidth = width
+            currentCarouselHeight = height
+            mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight)
+            updatePageIndicatorLocation()
+        }
+    }
+
+    private fun maybeResetSettingsCog() {
+        val hostStates = mediaHostStatesManager.mediaHostStates
+        val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
+                ?: true
+        val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
+                ?: endShowsActive
+        if (currentlyShowingOnlyActive != endShowsActive ||
+                ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
+                            startShowsActive != endShowsActive)) {
+            /// Whenever we're transitioning from between differing states or the endstate differs
+            // we reset the translation
+            currentlyShowingOnlyActive = endShowsActive
+            mediaCarouselScrollHandler.resetTranslation(animate = true)
         }
     }
 
@@ -404,6 +385,15 @@
                 }
                 mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
             }
+            mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
+            mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
+            val nowVisible = it.visible
+            if (nowVisible != playersVisible) {
+                playersVisible = nowVisible
+                if (nowVisible) {
+                    mediaCarouselScrollHandler.resetTranslation()
+                }
+            }
             updateCarouselSize()
         }
     }
@@ -420,16 +410,7 @@
             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
-            }
-            mediaCarousel.scrollX = newScroll
+            mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
             // Let's remeasure the carousel
             val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
             val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
new file mode 100644
index 0000000..993c05f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
@@ -0,0 +1,516 @@
+/*
+ * 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.Outline
+import android.util.MathUtils
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import androidx.core.view.GestureDetectorCompat
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.settingslib.Utils
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.qs.PageIndicator
+import com.android.systemui.R
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.util.animation.PhysicsAnimator
+import com.android.systemui.util.concurrency.DelayableExecutor
+
+private const val FLING_SLOP = 1000000
+private const val DISMISS_DELAY = 100L
+private const val RUBBERBAND_FACTOR = 0.2f
+private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
+
+/**
+ * Default spring configuration to use for animations where stiffness and/or damping ratio
+ * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
+ */
+private val translationConfig = PhysicsAnimator.SpringConfig(
+        SpringForce.STIFFNESS_MEDIUM,
+        SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+
+/**
+ * A controller class for the media scrollview, responsible for touch handling
+ */
+class MediaCarouselScrollHandler(
+    private val scrollView: MediaScrollView,
+    private val pageIndicator: PageIndicator,
+    private val mainExecutor: DelayableExecutor,
+    private val dismissCallback: () -> Unit,
+    private var translationChangedListener: () -> Unit,
+    private val falsingManager: FalsingManager
+) {
+    /**
+     * Do we need falsing protection?
+     */
+    var falsingProtectionNeeded: Boolean = false
+    /**
+     * The width of the carousel
+     */
+    private var carouselWidth: Int = 0
+
+    /**
+     * The height of the carousel
+     */
+    private var carouselHeight: Int = 0
+
+    /**
+     * How much are we scrolled into the current media?
+     */
+    private var cornerRadius: Int = 0
+
+    /**
+     * The content where the players are added
+     */
+    private var mediaContent: ViewGroup
+    /**
+     * The gesture detector to detect touch gestures
+     */
+    private val gestureDetector: GestureDetectorCompat
+
+    /**
+     * The settings button view
+     */
+    private lateinit var settingsButton: View
+
+    /**
+     * What's the currently active player index?
+     */
+    var activeMediaIndex: Int = 0
+        private set
+    /**
+     * How much are we scrolled into the current media?
+     */
+    private var scrollIntoCurrentMedia: Int = 0
+
+    /**
+     * how much is the content translated in X
+     */
+    var contentTranslation = 0.0f
+        private set(value) {
+            field = value
+            mediaContent.translationX = value
+            updateSettingsPresentation()
+            translationChangedListener.invoke()
+            updateClipToOutline()
+        }
+
+    /**
+     * The width of a player including padding
+     */
+    var playerWidthPlusPadding: Int = 0
+        set(value) {
+            field = value
+            // 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
+            }
+            scrollView.scrollX = newScroll
+        }
+
+    /**
+     * Does the dismiss currently show the setting cog?
+     */
+    var showsSettingsButton: Boolean = false
+
+    /**
+     * A utility to detect gestures, used in the touch listener
+     */
+    private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
+        override fun onFling(
+            eStart: MotionEvent?,
+            eCurrent: MotionEvent?,
+            vX: Float,
+            vY: Float
+        ) = onFling(vX, vY)
+
+        override fun onScroll(
+            down: MotionEvent?,
+            lastMotion: MotionEvent?,
+            distanceX: Float,
+            distanceY: Float
+        ) = onScroll(down!!, lastMotion!!, distanceX)
+
+        override fun onDown(e: MotionEvent?): Boolean {
+            if (falsingProtectionNeeded) {
+                falsingManager.onNotificationStartDismissing()
+            }
+            return false
+        }
+    }
+
+    /**
+     * The touch listener for the scroll view
+     */
+    private val touchListener = object : Gefingerpoken {
+        override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
+        override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
+    }
+
+    /**
+     * A listener that is invoked when the scrolling changes to update player visibilities
+     */
+    private val scrollChangedListener = object : View.OnScrollChangeListener {
+        override fun onScrollChange(
+            v: View?,
+            scrollX: Int,
+            scrollY: Int,
+            oldScrollX: Int,
+            oldScrollY: Int
+        ) {
+            if (playerWidthPlusPadding == 0) {
+                return
+            }
+            onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
+                    scrollX % playerWidthPlusPadding)
+        }
+    }
+
+    init {
+        gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener)
+        scrollView.touchListener = touchListener
+        scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
+        mediaContent = scrollView.contentContainer
+        scrollView.setOnScrollChangeListener(scrollChangedListener)
+        scrollView.outlineProvider = object : ViewOutlineProvider() {
+            override fun getOutline(view: View?, outline: Outline?) {
+                outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat())
+            }
+        }
+    }
+
+    fun onSettingsButtonUpdated(button: View) {
+        settingsButton = button
+        // We don't have a context to resolve, lets use the settingsbuttons one since that is
+        // reinflated appropriately
+        cornerRadius = settingsButton.resources.getDimensionPixelSize(
+                Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius))
+        updateSettingsPresentation()
+        scrollView.invalidateOutline()
+    }
+
+    private fun updateSettingsPresentation() {
+        if (showsSettingsButton) {
+            val settingsOffset = MathUtils.map(
+                    0.0f,
+                    getMaxTranslation().toFloat(),
+                    0.0f,
+                    1.0f,
+                    Math.abs(contentTranslation))
+            val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width *
+                    SETTINGS_BUTTON_TRANSLATION_FRACTION
+            val newTranslationX: Float
+            if (contentTranslation > 0) {
+                newTranslationX = settingsTranslation
+            } else {
+                newTranslationX = scrollView.width - settingsTranslation - settingsButton.width
+            }
+            val rotation = (1.0f - settingsOffset) * 50
+            settingsButton.rotation = rotation * -Math.signum(contentTranslation)
+            val alpha = MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)
+            settingsButton.alpha = alpha
+            settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE
+            settingsButton.translationX = newTranslationX
+            settingsButton.translationY = (scrollView.height - settingsButton.height) / 2.0f
+        } else {
+            settingsButton.visibility = View.INVISIBLE
+        }
+    }
+
+    private fun onTouch(motionEvent: MotionEvent): Boolean {
+        val isUp = motionEvent.action == MotionEvent.ACTION_UP
+        if (isUp && falsingProtectionNeeded) {
+            falsingManager.onNotificationStopDismissing()
+        }
+        if (gestureDetector.onTouchEvent(motionEvent)) {
+            if (isUp) {
+                // If this is an up and we're flinging, we don't want to have this touch reach
+                // the view, otherwise that would scroll, while we are trying to snap to the
+                // new page. Let's dispatch a cancel instead.
+                scrollView.cancelCurrentScroll()
+                return true
+            } else {
+                // Pass touches to the scrollView
+                return false
+            }
+        }
+        if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
+            // It's an up and the fling didn't take it above
+            val pos = scrollView.scrollX % playerWidthPlusPadding
+            val scollXAmount: Int
+            if (pos > playerWidthPlusPadding / 2) {
+                scollXAmount = playerWidthPlusPadding - pos
+            } else {
+                scollXAmount = -1 * pos
+            }
+            if (scollXAmount != 0) {
+                // Delay the scrolling since scrollView calls springback which cancels
+                // the animation again..
+                mainExecutor.execute {
+                    scrollView.smoothScrollBy(scollXAmount, 0)
+                }
+            }
+            val currentTranslation = scrollView.getContentTranslation()
+            if (currentTranslation != 0.0f) {
+                // We started a Swipe but didn't end up with a fling. Let's either go to the
+                // dismissed position or go back.
+                val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2
+                        || isFalseTouch()
+                val newTranslation: Float
+                if (springBack) {
+                    newTranslation = 0.0f
+                } else {
+                    newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
+                    if (!showsSettingsButton) {
+                        // Delay the dismiss a bit to avoid too much overlap. Waiting until the
+                        // animation has finished also feels a bit too slow here.
+                        mainExecutor.executeDelayed({
+                            dismissCallback.invoke()
+                        }, DISMISS_DELAY)
+                    }
+                }
+                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
+                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
+                scrollView.animationTargetX = newTranslation
+            }
+        }
+        // Always pass touches to the scrollView
+        return false
+    }
+
+    private fun isFalseTouch() = falsingProtectionNeeded && falsingManager.isFalseTouch
+
+    private fun getMaxTranslation() = if (showsSettingsButton) {
+            settingsButton.width
+        } else {
+            playerWidthPlusPadding
+        }
+
+    private fun onInterceptTouch(motionEvent: MotionEvent): Boolean {
+        return gestureDetector.onTouchEvent(motionEvent)
+    }
+
+    fun onScroll(down: MotionEvent,
+                 lastMotion: MotionEvent,
+                 distanceX: Float): Boolean {
+        val totalX = lastMotion.x - down.x
+        val currentTranslation = scrollView.getContentTranslation()
+        if (currentTranslation != 0.0f ||
+                !scrollView.canScrollHorizontally((-totalX).toInt())) {
+            var newTranslation = currentTranslation - distanceX
+            val absTranslation = Math.abs(newTranslation)
+            if (absTranslation > getMaxTranslation()) {
+                // Rubberband all translation above the maximum
+                if (Math.signum(distanceX) != Math.signum(currentTranslation)) {
+                    // The movement is in the same direction as our translation,
+                    // Let's rubberband it.
+                    if (Math.abs(currentTranslation) > getMaxTranslation()) {
+                        // we were already overshooting before. Let's add the distance
+                        // fully rubberbanded.
+                        newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
+                    } else {
+                        // We just crossed the boundary, let's rubberband it all
+                        newTranslation = Math.signum(newTranslation) * (getMaxTranslation() +
+                                (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
+                    }
+                } // Otherwise we don't have do do anything, and will remove the unrubberbanded
+                // translation
+            }
+            if (Math.signum(newTranslation) != Math.signum(currentTranslation)
+                    && currentTranslation != 0.0f) {
+                // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
+                // to scroll into the new direction
+                if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
+                    // We can actually scroll in the direction where we want to translate,
+                    // Let's make sure to stop at 0
+                    newTranslation = 0.0f
+                }
+            }
+            val physicsAnimator = PhysicsAnimator.getInstance(this)
+            if (physicsAnimator.isRunning()) {
+                physicsAnimator.spring(CONTENT_TRANSLATION,
+                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
+            } else {
+                contentTranslation = newTranslation
+            }
+            scrollView.animationTargetX = newTranslation
+            return true
+        }
+        return false
+    }
+
+    private fun onFling(
+        vX: Float,
+        vY: Float
+    ): Boolean {
+        if (vX * vX < 0.5 * vY * vY) {
+            return false
+        }
+        if (vX * vX < FLING_SLOP) {
+            return false
+        }
+        val currentTranslation = scrollView.getContentTranslation()
+        if (currentTranslation != 0.0f) {
+            // We're translated and flung. Let's see if the fling is in the same direction
+            val newTranslation: Float
+            if (Math.signum(vX) != Math.signum(currentTranslation) || isFalseTouch()) {
+                // The direction of the fling isn't the same as the translation, let's go to 0
+                newTranslation = 0.0f
+            } else {
+                newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
+                // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
+                // has finished also feels a bit too slow here.
+                if (!showsSettingsButton) {
+                    mainExecutor.executeDelayed({
+                        dismissCallback.invoke()
+                    }, DISMISS_DELAY)
+                }
+            }
+            PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
+                    newTranslation, startVelocity = vX, config = translationConfig).start()
+            scrollView.animationTargetX = newTranslation
+        } else {
+            // We're flinging the player! Let's go either to the previous or to the next player
+            val pos = scrollView.scrollX
+            val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
+            var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex
+            destIndex = Math.max(0, destIndex)
+            destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
+            val view = mediaContent.getChildAt(destIndex)
+            // We need to post this since we're dispatching a touch to the underlying view to cancel
+            // but canceling will actually abort the animation.
+            mainExecutor.execute {
+                scrollView.smoothScrollTo(view.left, scrollView.scrollY)
+            }
+        }
+        return true
+    }
+
+    /**
+     * Reset the translation of the players when swiped
+     */
+    fun resetTranslation(animate: Boolean = false) {
+        if (scrollView.getContentTranslation() != 0.0f) {
+            if (animate) {
+                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
+                        0.0f, config = translationConfig).start()
+                scrollView.animationTargetX = 0.0f
+            } else {
+                PhysicsAnimator.getInstance(this).cancel()
+                contentTranslation = 0.0f
+            }
+        }
+    }
+
+    private fun updateClipToOutline() {
+        val clip = contentTranslation != 0.0f || scrollIntoCurrentMedia != 0
+        scrollView.clipToOutline = clip
+    }
+
+    private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
+        val wasScrolledIn = scrollIntoCurrentMedia != 0
+        scrollIntoCurrentMedia = scrollInAmount
+        val nowScrolledIn = scrollIntoCurrentMedia != 0
+        if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
+            activeMediaIndex = newIndex
+            updatePlayerVisibilities()
+        }
+        val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
+            scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
+        pageIndicator.setLocation(location)
+        updateClipToOutline()
+    }
+
+    /**
+     * Notified whenever the players or their order has changed
+     */
+    fun onPlayersChanged() {
+        updatePlayerVisibilities()
+        updateMediaPaddings()
+    }
+
+    private fun updateMediaPaddings() {
+        val padding = scrollView.context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+        val childCount = mediaContent.childCount
+        for (i in 0 until childCount) {
+            val mediaView = mediaContent.getChildAt(i)
+            val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
+            val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
+            if (layoutParams.marginEnd != desiredPaddingEnd) {
+                layoutParams.marginEnd = desiredPaddingEnd
+                mediaView.layoutParams = layoutParams
+            }
+        }
+    }
+
+    private fun updatePlayerVisibilities() {
+        val scrolledIn = scrollIntoCurrentMedia != 0
+        for (i in 0 until mediaContent.childCount) {
+            val view = mediaContent.getChildAt(i)
+            val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
+            view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
+        }
+    }
+
+    /**
+     * Notify that a player will be removed right away. This gives us the opporunity to look
+     * where it was and update our scroll position.
+     */
+    fun onPrePlayerRemoved(removed: MediaControlPanel) {
+        val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex
+        if (beforeActive) {
+            // also update the index here since the scroll below might not always lead
+            // to a scrolling changed
+            activeMediaIndex = Math.max(0, activeMediaIndex - 1)
+            scrollView.scrollX = Math.max(scrollView.scrollX -
+                    playerWidthPlusPadding, 0)
+        }
+    }
+
+    /**
+     * Update the bounds of the carousel
+     */
+    fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
+        if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
+            carouselWidth = currentCarouselWidth
+            carouselHeight = currentCarouselHeight
+            scrollView.invalidateOutline()
+        }
+    }
+
+    companion object {
+        private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>(
+                "contentTranslation") {
+            override fun getValue(handler: MediaCarouselScrollHandler): Float {
+                return handler.contentTranslation
+            }
+
+            override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
+                handler.contentTranslation = value
+            }
+        }
+    }
+}
\ 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 c80c82f..3fc162e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -20,13 +20,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.ColorStateList;
-import android.graphics.Color;
 import android.graphics.Outline;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
-import android.graphics.drawable.RippleDrawable;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
@@ -60,6 +57,7 @@
  */
 public class MediaControlPanel {
     private static final String TAG = "MediaControlPanel";
+    private static final float DISABLED_ALPHA = 0.38f;
 
     // Button IDs for QS controls
     static final int[] ACTION_IDS = {
@@ -258,12 +256,6 @@
         ImageView iconView = mViewHolder.getSeamlessIcon();
         TextView deviceName = mViewHolder.getSeamlessText();
 
-        // Update the outline color
-        RippleDrawable bkgDrawable = (RippleDrawable) mViewHolder.getSeamless().getForeground();
-        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
-        rect.setStroke(2, deviceName.getCurrentTextColor());
-        rect.setColor(Color.TRANSPARENT);
-
         final MediaDeviceData device = data.getDevice();
         final int seamlessId = mViewHolder.getSeamless().getId();
         final int seamlessFallbackId = mViewHolder.getSeamlessFallback().getId();
@@ -276,6 +268,11 @@
         mViewHolder.getSeamless().setVisibility(seamlessVisibility);
         expandedSet.setVisibility(seamlessId, seamlessVisibility);
         collapsedSet.setVisibility(seamlessId, seamlessVisibility);
+        final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f;
+        expandedSet.setAlpha(seamlessId, seamlessAlpha);
+        collapsedSet.setAlpha(seamlessId, seamlessAlpha);
+        // Disable clicking on output switcher for resumption controls.
+        mViewHolder.getSeamless().setEnabled(!data.getResumption());
         if (showFallback) {
             iconView.setImageDrawable(null);
             deviceName.setText(null);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index 0b0ffce..8c9cb1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -81,6 +81,11 @@
      */
     var resumeAction: Runnable?,
     /**
+     * Indicates that this player is a resumption player (ie. It only shows a play actions which
+     * will start the app and start playing).
+     */
+    var resumption: Boolean = false,
+    /**
      * Notification key for cancelling a media player after a timeout (when not using resumption.)
      */
     val notificationKey: String? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index c59a548..416c81a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -323,7 +323,7 @@
             onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName,
                     null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
                     packageName, token, appIntent, device = null, active = false,
-                    resumeAction = resumeAction, notificationKey = packageName,
+                    resumeAction = resumeAction, resumption = true, notificationKey = packageName,
                     hasCheckedForResume = true))
         }
     }
@@ -542,7 +542,7 @@
             val data = mediaEntries.remove(key)!!
             val resumeAction = getResumeMediaAction(data.resumeAction!!)
             val updated = data.copy(token = null, actions = listOf(resumeAction),
-                actionsToShowInCompact = listOf(0), active = false)
+                    actionsToShowInCompact = listOf(0), active = false, resumption = true)
             mediaEntries.put(data.packageName, updated)
             // Notify listeners of "new" controls
             val listenersCopy = listeners.toSet()
@@ -592,6 +592,16 @@
         }
     }
 
+    /**
+     * Invoked when the user has dismissed the media carousel
+     */
+    fun onSwipeToDismiss() {
+        val mediaKeys = mediaEntries.keys.toSet()
+        mediaKeys.forEach {
+            setTimedOut(it, timedOut = true)
+        }
+    }
+
     interface Listener {
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 26fa296..3266074 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -49,7 +49,7 @@
     private val statusBarStateController: SysuiStatusBarStateController,
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
-    private val mediaViewManager: MediaViewManager,
+    private val mediaCarouselController: MediaCarouselController,
     private val notifLockscreenUserManager: NotificationLockscreenUserManager,
     wakefulnessLifecycle: WakefulnessLifecycle
 ) {
@@ -65,7 +65,7 @@
     private var animationStartBounds: Rect = Rect()
     private var targetBounds: Rect = Rect()
     private val mediaFrame
-        get() = mediaViewManager.mediaFrame
+        get() = mediaCarouselController.mediaFrame
     private var statusbarState: Int = statusBarStateController.state
     private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
         interpolator = Interpolators.FAST_OUT_SLOW_IN
@@ -227,6 +227,10 @@
     fun register(mediaObject: MediaHost): UniqueObjectHostView {
         val viewHost = createUniqueObjectHost()
         mediaObject.hostView = viewHost
+        mediaObject.addVisibilityChangeListener {
+            // Never animate because of a visibility change, only state changes should do that
+            updateDesiredLocation(forceNoAnimation = true)
+        }
         mediaHosts[mediaObject.location] = mediaObject
         if (mediaObject.location == desiredLocation) {
             // In case we are overriding a view that is already visible, make sure we attach it
@@ -260,8 +264,10 @@
     /**
      * Updates the location that the view should be in. If it changes, an animation may be triggered
      * going from the old desired location to the new one.
+     *
+     * @param forceNoAnimation optional parameter telling the system not to animate
      */
-    private fun updateDesiredLocation() {
+    private fun updateDesiredLocation(forceNoAnimation: Boolean = false) {
         val desiredLocation = calculateLocation()
         if (desiredLocation != this.desiredLocation) {
             if (this.desiredLocation >= 0) {
@@ -270,11 +276,12 @@
             val isNewView = this.desiredLocation == -1
             this.desiredLocation = desiredLocation
             // Let's perform a transition
-            val animate = shouldAnimateTransition(desiredLocation, previousLocation)
+            val animate = !forceNoAnimation &&
+                    shouldAnimateTransition(desiredLocation, previousLocation)
             val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
             val host = getHost(desiredLocation)
-            mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
-                    delay)
+            mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
+                    animDuration, delay)
             performTransitionToNewLocation(isNewView, animate)
         }
     }
@@ -457,7 +464,7 @@
         val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
         val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
         val endLocation = desiredLocation
-        mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately)
+        mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
         updateHostAttachment()
         if (currentAttachmentLocation == IN_OVERLAY) {
             mediaFrame.setLeftTopRightBottom(
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 7c5f0d1..1ae9d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -2,6 +2,7 @@
 
 import android.graphics.PointF
 import android.graphics.Rect
+import android.util.ArraySet
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
 import com.android.systemui.util.animation.MeasurementInput
@@ -20,7 +21,7 @@
     lateinit var hostView: UniqueObjectHostView
     var location: Int = -1
         private set
-    var visibleChangedListener: ((Boolean) -> Unit)? = null
+    private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet()
 
     private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
 
@@ -58,6 +59,10 @@
         }
     }
 
+    fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
+        visibleChangedListeners.add(listener)
+    }
+
     /**
      * 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
@@ -113,8 +118,13 @@
         } else {
             mediaDataManager.hasAnyMedia()
         }
-        hostView.visibility = if (visible) View.VISIBLE else View.GONE
-        visibleChangedListener?.invoke(visible)
+        val newVisibility = if (visible) View.VISIBLE else View.GONE
+        if (newVisibility != hostView.visibility) {
+            hostView.visibility = newVisibility
+            visibleChangedListeners.forEach {
+                it.invoke(visible)
+            }
+        }
     }
 
     class MediaHostStateHolder @Inject constructor() : MediaHostState {
@@ -153,6 +163,15 @@
                 changedListener?.invoke()
             }
 
+        override var falsingProtectionNeeded: Boolean = false
+            set(value) {
+                if (field == value) {
+                    return
+                }
+                field = value
+                changedListener?.invoke()
+            }
+
         override fun getPivotX(): Float = gonePivot.x
         override fun getPivotY(): Float = gonePivot.y
         override fun setGonePivot(x: Float, y: Float) {
@@ -178,6 +197,7 @@
             mediaHostState.measurementInput = measurementInput?.copy()
             mediaHostState.visible = visible
             mediaHostState.gonePivot.set(gonePivot)
+            mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
             return mediaHostState
         }
 
@@ -197,6 +217,9 @@
             if (visible != other.visible) {
                 return false
             }
+            if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
+                return false
+            }
             if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
                 return false
             }
@@ -206,6 +229,7 @@
         override fun hashCode(): Int {
             var result = measurementInput?.hashCode() ?: 0
             result = 31 * result + expansion.hashCode()
+            result = 31 * result + falsingProtectionNeeded.hashCode()
             result = 31 * result + showsOnlyActiveMedia.hashCode()
             result = 31 * result + if (visible) 1 else 2
             result = 31 * result + gonePivot.hashCode()
@@ -239,6 +263,11 @@
     var visible: Boolean
 
     /**
+     * Does this host need any falsing protection?
+     */
+    var falsingProtectionNeeded: Boolean
+
+    /**
      * Sets the pivot point when clipping the height or width.
      * Clipping happens when animating visibility when we're visible in QS but not on QQS,
      * for example.
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
new file mode 100644
index 0000000..a079b06
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.media
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.HorizontalScrollView
+import com.android.systemui.Gefingerpoken
+import com.android.systemui.util.animation.physicsAnimator
+
+/**
+ * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful
+ * when only measuring children but not the parent, when trying to apply a new scroll position
+ */
+class MediaScrollView @JvmOverloads constructor(
+    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
+    : HorizontalScrollView(context, attrs, defStyleAttr) {
+
+    lateinit var contentContainer: ViewGroup
+        private set
+    var touchListener: Gefingerpoken? = null
+
+    /**
+     * The target value of the translation X animation. Only valid if the physicsAnimator is running
+     */
+    var animationTargetX = 0.0f
+
+    /**
+     * Get the current content translation. This is usually the normal translationX of the content,
+     * but when animating, it might differ
+     */
+    fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) {
+        animationTargetX
+    } else {
+        contentContainer.translationX
+    }
+
+    /**
+     * Allow all scrolls to go through, use base implementation
+     */
+    override fun scrollTo(x: Int, y: Int) {
+        if (mScrollX != x || mScrollY != y) {
+            val oldX: Int = mScrollX
+            val oldY: Int = mScrollY
+            mScrollX = x
+            mScrollY = y
+            invalidateParentCaches()
+            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
+            if (!awakenScrollBars()) {
+                postInvalidateOnAnimation()
+            }
+        }
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+        var intercept = false;
+        touchListener?.let {
+            intercept = it.onInterceptTouchEvent(ev)
+        }
+        return super.onInterceptTouchEvent(ev) || intercept;
+    }
+
+    override fun onTouchEvent(ev: MotionEvent?): Boolean {
+        var touch = false;
+        touchListener?.let {
+            touch = it.onTouchEvent(ev)
+        }
+        return super.onTouchEvent(ev) || touch
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        contentContainer = getChildAt(0) as ViewGroup
+    }
+
+    override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int,
+                              scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int,
+                              maxOverScrollY: Int, isTouchEvent: Boolean): Boolean {
+        if (getContentTranslation() != 0.0f) {
+            // When we're dismissing we ignore all the scrolling
+            return false
+        }
+        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
+                scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
+    }
+
+    /**
+     * Cancel the current touch event going on.
+     */
+    fun cancelCurrentScroll() {
+        val now = SystemClock.uptimeMillis()
+        val event = MotionEvent.obtain(now, now,
+                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
+        event.source = InputDevice.SOURCE_TOUCHSCREEN
+        super.onTouchEvent(event)
+        event.recycle()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index 90ccfc6..fc22c02 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -17,9 +17,11 @@
 package com.android.systemui.media
 
 import android.content.Context
+import android.content.res.Configuration
 import android.graphics.PointF
 import androidx.constraintlayout.widget.ConstraintSet
 import com.android.systemui.R
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.MeasurementOutput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionLayoutController
@@ -32,9 +34,14 @@
  */
 class MediaViewController @Inject constructor(
     context: Context,
+    private val configurationController: ConfigurationController,
     private val mediaHostStatesManager: MediaHostStatesManager
 ) {
 
+    /**
+     * A listener when the current dimensions of the player change
+     */
+    lateinit var sizeChangedListener: () -> Unit
     private var firstRefresh: Boolean = true
     private var transitionLayout: TransitionLayout? = null
     private val layoutController = TransitionLayoutController()
@@ -52,12 +59,14 @@
      * 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
 
     /**
@@ -76,10 +85,42 @@
     private val tmpPoint = PointF()
 
     /**
+     * The current width of the player. This might not factor in case the player is animating
+     * to the current state, but represents the end state
+     */
+    var currentWidth: Int = 0
+    /**
+     * The current height of the player. This might not factor in case the player is animating
+     * to the current state, but represents the end state
+     */
+    var currentHeight: Int = 0
+
+    /**
+     * A callback for RTL config changes
+     */
+    private val configurationListener = object : ConfigurationController.ConfigurationListener {
+        override fun onConfigChanged(newConfig: Configuration?) {
+            // Because the TransitionLayout is not always attached (and calculates/caches layout
+            // results regardless of attach state), we have to force the layoutDirection of the view
+            // to the correct value for the user's current locale to ensure correct recalculation
+            // when/after calling refreshState()
+            newConfig?.apply {
+                if (transitionLayout?.rawLayoutDirection != layoutDirection) {
+                    transitionLayout?.layoutDirection = layoutDirection
+                    refreshState()
+                }
+            }
+        }
+    }
+
+    /**
      * A callback for media state changes
      */
     val stateCallback = object : MediaHostStatesManager.Callback {
-        override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+        override fun onHostStateChanged(
+            @MediaLocation location: Int,
+            mediaHostState: MediaHostState
+        ) {
             if (location == currentEndLocation || location == currentStartLocation) {
                 setCurrentState(currentStartLocation,
                         currentEndLocation,
@@ -105,6 +146,12 @@
         collapsedLayout.load(context, R.xml.media_collapsed)
         expandedLayout.load(context, R.xml.media_expanded)
         mediaHostStatesManager.addController(this)
+        layoutController.sizeChangedListener = { width: Int, height: Int ->
+            currentWidth = width
+            currentHeight = height
+            sizeChangedListener.invoke()
+        }
+        configurationController.addCallback(configurationListener)
     }
 
     /**
@@ -112,6 +159,7 @@
      */
     fun onDestroy() {
         mediaHostStatesManager.removeController(this)
+        configurationController.removeCallback(configurationListener)
     }
 
     private fun ensureAllMeasurements() {
@@ -279,6 +327,8 @@
                     tmpPoint, tmpState)
             tmpState
         }
+        currentWidth = result.width
+        currentHeight = result.height
         layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
                 animationDelay)
     }
@@ -324,7 +374,7 @@
             // Let's clear all of our measurements and recreate them!
             viewStates.clear()
             setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
-                    applyImmediately = false)
+                    applyImmediately = true)
         }
         firstRefresh = false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
index 9ede083..600fdc2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -48,6 +48,7 @@
 
     // Seek bar
     val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)
+    val progressTimes = itemView.requireViewById<ViewGroup>(R.id.notification_media_progress_time)
     val elapsedTimeView = itemView.requireViewById<TextView>(R.id.media_elapsed_time)
     val totalTimeView = itemView.requireViewById<TextView>(R.id.media_total_time)
 
@@ -93,8 +94,16 @@
          * @param parent Parent of inflated view.
          */
         @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder {
-            val v = inflater.inflate(R.layout.media_view, parent, false)
-            return PlayerViewHolder(v)
+            val mediaView = inflater.inflate(R.layout.media_view, parent, false)
+            // Because this media view (a TransitionLayout) is used to measure and layout the views
+            // in various states before being attached to its parent, we can't depend on the default
+            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
+            mediaView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+            return PlayerViewHolder(mediaView).apply {
+                // Media playback is in the direction of tape, not time, so it stays LTR
+                seekBar.layoutDirection = View.LAYOUT_DIRECTION_LTR
+                progressTimes.layoutDirection = View.LAYOUT_DIRECTION_LTR
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt
deleted file mode 100644
index 8efc954..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.android.systemui.media
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.HorizontalScrollView
-
-/**
- * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful
- * when only measuring children but not the parent, when trying to apply a new scroll position
- */
-class UnboundHorizontalScrollView @JvmOverloads constructor(
-    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
-    : HorizontalScrollView(context, attrs, defStyleAttr) {
-
-    /**
-     * Allow all scrolls to go through, use base implementation
-     */
-    override fun scrollTo(x: Int, y: Int) {
-        if (mScrollX != x || mScrollY != y) {
-            val oldX: Int = mScrollX
-            val oldY: Int = mScrollY
-            mScrollX = x
-            mScrollY = y
-            invalidateParentCaches()
-            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
-            if (!awakenScrollBars()) {
-                postInvalidateOnAnimation()
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
index aa17c4a..309b32f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
@@ -46,6 +46,11 @@
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
                     }
+                } else if (!canScrollVertically(-1)) {
+                    // Don't pass on the touch to the view, because scrolling will unconditionally
+                    // disallow interception even if we can't scroll.
+                    // if a user can't scroll at all, we should never listen to the touch.
+                    return false;
                 }
                 break;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index bc8f5a8..e66b33c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -18,7 +18,6 @@
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
 import android.view.View.OnLayoutChangeListener;
-import android.widget.ScrollView;
 
 import com.android.systemui.Dependency;
 import com.android.systemui.plugins.qs.QS;
@@ -300,10 +299,16 @@
             if (mQsPanel.getSecurityFooter() != null) {
                 builder.addFloat(mQsPanel.getSecurityFooter().getView(), "alpha", 0, 1);
             }
+            if (mQsPanel.getDivider() != null) {
+                builder.addFloat(mQsPanel.getDivider(), "alpha", 0, 1);
+            }
             mFirstPageDelayedAnimator = builder.build();
             if (mQsPanel.getSecurityFooter() != null) {
                 mAllViews.add(mQsPanel.getSecurityFooter().getView());
             }
+            if (mQsPanel.getDivider() != null) {
+                mAllViews.add(mQsPanel.getDivider());
+            }
             float px = 0;
             float py = 1;
             if (tiles.size() <= 3) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 0332bc3..6b12e47 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -26,8 +26,12 @@
 import android.view.View;
 import android.widget.FrameLayout;
 
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringForce;
+
 import com.android.systemui.R;
 import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.util.animation.PhysicsAnimator;
 
 /**
  * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
@@ -35,7 +39,22 @@
 public class QSContainerImpl extends FrameLayout {
 
     private final Point mSizePoint = new Point();
+    private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM =
+            new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") {
+                @Override
+                public float getValue(QSContainerImpl qsImpl) {
+                    return qsImpl.getBackgroundBottom();
+                }
 
+                @Override
+                public void setValue(QSContainerImpl background, float value) {
+                    background.setBackgroundBottom((int) value);
+                }
+            };
+    private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING
+            = new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM,
+            SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+    private int mBackgroundBottom = -1;
     private int mHeightOverride = -1;
     private QSPanel mQSPanel;
     private View mQSDetail;
@@ -53,6 +72,7 @@
     private boolean mQsDisabled;
     private int mContentPaddingStart = -1;
     private int mContentPaddingEnd = -1;
+    private boolean mAnimateBottomOnNextLayout;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -71,10 +91,30 @@
         mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
         mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
         updateResources();
+        mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> {
+            if (mHeader.getHeaderQsPanel().isShown()) {
+                mAnimateBottomOnNextLayout = true;
+            }
+        });
+
 
         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
     }
 
+    private void setBackgroundBottom(int value) {
+        // We're saving the bottom separately since otherwise the bottom would be overridden in
+        // the layout and the animation wouldn't properly start at the old position.
+        mBackgroundBottom = value;
+        mBackground.setBottom(value);
+    }
+
+    private float getBackgroundBottom() {
+        if (mBackgroundBottom == -1) {
+            return mBackground.getBottom();
+        }
+        return mBackgroundBottom;
+    }
+
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
@@ -140,7 +180,8 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        updateExpansion();
+        updateExpansion(mAnimateBottomOnNextLayout /* animate */);
+        mAnimateBottomOnNextLayout = false;
     }
 
     public void disable(int state1, int state2, boolean animate) {
@@ -181,13 +222,31 @@
     }
 
     public void updateExpansion() {
+        updateExpansion(false /* animate */);
+    }
+
+    public void updateExpansion(boolean animate) {
         int height = calculateContainerHeight();
         setBottom(getTop() + height);
         mQSDetail.setBottom(getTop() + height);
         // Pin the drag handle to the bottom of the panel.
         mDragHandle.setTranslationY(height - mDragHandle.getHeight());
         mBackground.setTop(mQSPanelContainer.getTop());
-        mBackground.setBottom(height);
+        updateBackgroundBottom(height, animate);
+    }
+
+    private void updateBackgroundBottom(int height, boolean animated) {
+        PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this);
+        if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) {
+            // An animation is running or we want to animate
+            // Let's make sure to set the currentValue again, since the call below might only
+            // start in the next frame and otherwise we'd flicker
+            BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this));
+            physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start();
+        } else {
+            BACKGROUND_BOTTOM.setValue(this, height);
+        }
+
     }
 
     protected int calculateContainerHeight() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 5021e00..c8a34f0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -65,6 +65,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -117,6 +118,8 @@
 
     @Nullable
     protected View mFooter;
+    @Nullable
+    protected View mDivider;
 
     @Nullable
     private ViewGroup mHeaderContainer;
@@ -141,6 +144,7 @@
     private int mLastOrientation = -1;
     private int mMediaTotalBottomMargin;
     private int mFooterMarginStartHorizontal;
+    private Consumer<Boolean> mMediaVisibilityChangedListener;
 
 
     @Inject
@@ -158,8 +162,8 @@
         mMediaTotalBottomMargin = getResources().getDimensionPixelSize(
                 R.dimen.quick_settings_bottom_margin_media);
         mMediaHost = mediaHost;
-        mMediaHost.setVisibleChangedListener((visible) -> {
-            switchTileLayout();
+        mMediaHost.addVisibilityChangeListener((visible) -> {
+            onMediaVisibilityChanged(visible);
             return null;
         });
         mContext = context;
@@ -207,6 +211,13 @@
         updateResources();
     }
 
+    protected void onMediaVisibilityChanged(Boolean visible) {
+        switchTileLayout();
+        if (mMediaVisibilityChangedListener != null) {
+            mMediaVisibilityChangedListener.accept(visible);
+        }
+    }
+
     protected void addSecurityFooter() {
         mSecurityFooter = new QSSecurityFooter(this, mContext);
     }
@@ -479,6 +490,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mFooter = findViewById(R.id.qs_footer);
+        mDivider = findViewById(R.id.divider);
         switchTileLayout(true /* force */);
     }
 
@@ -489,6 +501,13 @@
     private boolean switchTileLayout(boolean force) {
         /** Whether or not the QuickQSPanel currently contains a media player. */
         boolean horizontal = shouldUseHorizontalLayout();
+        if (mDivider != null) {
+            if (!horizontal && mUsingMediaPlayer && mMediaHost.getVisible()) {
+                mDivider.setVisibility(View.VISIBLE);
+            } else {
+                mDivider.setVisibility(View.GONE);
+            }
+        }
         if (horizontal != mUsingHorizontalLayout || force) {
             mUsingHorizontalLayout = horizontal;
             View visibleView = horizontal ? mHorizontalLinearLayout : (View) mRegularTileLayout;
@@ -522,6 +541,7 @@
             }
             updateTileLayoutMargins();
             updateFooterMargin();
+            updateDividerMargin();
             updateMediaHostContentMargins();
             updateHorizontalLinearLayoutMargins();
             updatePadding();
@@ -971,6 +991,11 @@
         return mSecurityFooter;
     }
 
+    @Nullable
+    public View getDivider() {
+        return mDivider;
+    }
+
     public void showDeviceMonitoringDialog() {
         if (mSecurityFooter != null) {
             mSecurityFooter.showDeviceMonitoringDialog();
@@ -986,6 +1011,7 @@
                 mContentMarginEnd - mVisualTilePadding);
         updateMediaHostContentMargins();
         updateFooterMargin();
+        updateDividerMargin();
     }
 
     private void updateFooterMargin() {
@@ -1027,6 +1053,11 @@
         updateMargins((View) mTileLayout, mVisualMarginStart, marginEnd);
     }
 
+    private void updateDividerMargin() {
+        if (mDivider == null) return;
+        updateMargins(mDivider, mContentMarginStart, mContentMarginEnd);
+    }
+
     /**
      * Update the margins of the media hosts
      */
@@ -1065,6 +1096,10 @@
         mHeaderContainer = headerContainer;
     }
 
+    public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) {
+        mMediaVisibilityChangedListener = visibilityChangedListener;
+    }
+
     private class H extends Handler {
         private static final int SHOW_DETAIL = 1;
         private static final int SET_TILE_VISIBILITY = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
index d057a8a..8347def 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
@@ -97,6 +97,9 @@
                 mModes);
         a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
         mOptions.setAdapter(a);
+        mOptions.setOnItemClickListenerInt((parent, view, position, id) -> {
+            mAudioSwitch.setChecked(true);
+        });
 
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index 33f0f4d..9bbc4dd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -1105,14 +1105,19 @@
                     return;
                 }
 
-                Intent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
+                PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
                 if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) {
                     ScreenshotNotificationsController.cancelScreenshotNotification(context);
                 }
                 ActivityOptions opts = ActivityOptions.makeBasic();
                 opts.setDisallowEnterPictureInPictureWhileLaunching(
                         intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
-                context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT);
+                try {
+                    actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
+                } catch (PendingIntent.CanceledException e) {
+                    Log.e(TAG, "Pending intent canceled", e);
+                }
+
             };
 
             if (mStatusBar != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 10e6902..e3fbdbc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -281,11 +281,13 @@
                 Intent.createChooser(sharingIntent, null, chooserAction.getIntentSender())
                         .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
                         .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, requestCode,
+                sharingChooserIntent, 0, null, UserHandle.CURRENT);
 
         // Create a share action for the notification
         PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
                 new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
-                        .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, sharingChooserIntent)
+                        .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent)
                         .putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true)
                         .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
                         .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
@@ -320,14 +322,17 @@
         editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
         editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
+        PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context, 0,
+                editIntent, 0, null, UserHandle.CURRENT);
+
         // Make sure pending intents for the system user are still unique across users
         // by setting the (otherwise unused) request code to the current user id.
         int requestCode = mContext.getUserId();
 
         // Create a edit action
-        PendingIntent editAction = PendingIntent.getBroadcast(context, requestCode,
+        PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
                 new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
-                        .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, editIntent)
+                        .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent)
                         .putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION,
                                 editIntent.getComponent() != null)
                         .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
@@ -335,7 +340,7 @@
                                 mSmartActionsEnabled)
                         .setAction(Intent.ACTION_EDIT)
                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
-                PendingIntent.FLAG_CANCEL_CURRENT);
+                PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
         Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
                 Icon.createWithResource(r, R.drawable.ic_screenshot_edit),
                 r.getString(com.android.internal.R.string.screenshot_edit), editAction);
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
index 03a0d93..ad31220 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java
@@ -387,6 +387,7 @@
         }
         // Always set this because we could be entering split when mMinimized is already true
         wct.setFocusable(mSplits.mPrimary.token, !mMinimized);
+        boolean onlyFocusable = true;
 
         // Update home-stack resizability
         final boolean homeResizableChanged = mHomeStackResizable != homeStackResizable;
@@ -395,6 +396,7 @@
             if (isDividerVisible()) {
                 WindowManagerProxy.applyHomeTasksMinimized(
                         mSplitLayout, mSplits.mSecondary.token, wct);
+                onlyFocusable = false;
             }
         }
 
@@ -416,7 +418,15 @@
             }
         }
         updateTouchable();
-        mWindowManagerProxy.applySyncTransaction(wct);
+        if (onlyFocusable) {
+            // If we are only setting focusability, a sync transaction isn't necessary (in fact it
+            // can interrupt other animations), so see if it can be submitted on pending instead.
+            if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
+                WindowOrganizer.applyTransaction(wct);
+            }
+        } else {
+            mWindowManagerProxy.applySyncTransaction(wct);
+        }
     }
 
     void setAdjustedForIme(boolean adjustedForIme) {
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
index d782a3ca..47c8c0a 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerImeController.java
@@ -29,6 +29,7 @@
 import android.window.TaskOrganizer;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
+import android.window.WindowOrganizer;
 
 import androidx.annotation.Nullable;
 
@@ -173,47 +174,51 @@
     }
 
     private void updateImeAdjustState() {
-        // Reposition the server's secondary split position so that it evaluates
-        // insets properly.
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        final SplitDisplayLayout splitLayout = getLayout();
-        if (mTargetAdjusted) {
-            splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
-            wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
-            // "Freeze" the configuration size so that the app doesn't get a config
-            // or relaunch. This is required because normally nav-bar contributes
-            // to configuration bounds (via nondecorframe).
-            Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
-                    .windowConfiguration.getAppBounds());
-            adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
-                    - splitLayout.mSecondary.top);
-            wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
-            wct.setScreenSizeDp(mSplits.mSecondary.token,
-                    mSplits.mSecondary.configuration.screenWidthDp,
-                    mSplits.mSecondary.configuration.screenHeightDp);
+        if (mAdjusted != mTargetAdjusted) {
+            // Reposition the server's secondary split position so that it evaluates
+            // insets properly.
+            WindowContainerTransaction wct = new WindowContainerTransaction();
+            final SplitDisplayLayout splitLayout = getLayout();
+            if (mTargetAdjusted) {
+                splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop);
+                wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary);
+                // "Freeze" the configuration size so that the app doesn't get a config
+                // or relaunch. This is required because normally nav-bar contributes
+                // to configuration bounds (via nondecorframe).
+                Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration
+                        .windowConfiguration.getAppBounds());
+                adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top
+                        - splitLayout.mSecondary.top);
+                wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds);
+                wct.setScreenSizeDp(mSplits.mSecondary.token,
+                        mSplits.mSecondary.configuration.screenWidthDp,
+                        mSplits.mSecondary.configuration.screenHeightDp);
 
-            wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
-            adjustAppBounds = new Rect(mSplits.mPrimary.configuration
-                    .windowConfiguration.getAppBounds());
-            adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
-                    - splitLayout.mPrimary.top);
-            wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
-            wct.setScreenSizeDp(mSplits.mPrimary.token,
-                    mSplits.mPrimary.configuration.screenWidthDp,
-                    mSplits.mPrimary.configuration.screenHeightDp);
-        } else {
-            wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
-            wct.setAppBounds(mSplits.mSecondary.token, null);
-            wct.setScreenSizeDp(mSplits.mSecondary.token,
-                    SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
-            wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
-            wct.setAppBounds(mSplits.mPrimary.token, null);
-            wct.setScreenSizeDp(mSplits.mPrimary.token,
-                    SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+                wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary);
+                adjustAppBounds = new Rect(mSplits.mPrimary.configuration
+                        .windowConfiguration.getAppBounds());
+                adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top
+                        - splitLayout.mPrimary.top);
+                wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds);
+                wct.setScreenSizeDp(mSplits.mPrimary.token,
+                        mSplits.mPrimary.configuration.screenWidthDp,
+                        mSplits.mPrimary.configuration.screenHeightDp);
+            } else {
+                wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary);
+                wct.setAppBounds(mSplits.mSecondary.token, null);
+                wct.setScreenSizeDp(mSplits.mSecondary.token,
+                        SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+                wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary);
+                wct.setAppBounds(mSplits.mPrimary.token, null);
+                wct.setScreenSizeDp(mSplits.mPrimary.token,
+                        SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED);
+            }
+
+            if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) {
+                WindowOrganizer.applyTransaction(wct);
+            }
         }
 
-        mSplits.mDivider.getWmProxy().applySyncTransaction(wct);
-
         // Update all the adjusted-for-ime states
         if (!mPaused) {
             final DividerView view = getView();
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 42d8c95..6f554e6 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -352,8 +352,6 @@
                 minimizeLeft + mMinimizedShadow.getMeasuredWidth(),
                 minimizeTop + mMinimizedShadow.getMeasuredHeight());
         if (changed) {
-            mWindowManagerProxy.setTouchRegion(new Rect(mHandle.getLeft(), mHandle.getTop(),
-                    mHandle.getRight(), mHandle.getBottom()));
             notifySplitScreenBoundsChanged();
         }
     }
@@ -679,6 +677,14 @@
     private void notifySplitScreenBoundsChanged() {
         mOtherTaskRect.set(mSplitLayout.mSecondary);
 
+        mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom());
+        if (isHorizontalDivision()) {
+            mTmpRect.offsetTo(0, mDividerPositionY);
+        } else {
+            mTmpRect.offsetTo(mDividerPositionX, 0);
+        }
+        mWindowManagerProxy.setTouchRegion(mTmpRect);
+
         mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets());
         switch (mSplitLayout.getPrimarySplitSide()) {
             case WindowManager.DOCKED_LEFT:
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 8c7e071..a144453 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.UserInfo;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.hardware.biometrics.BiometricSourceType;
@@ -34,12 +35,16 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.text.format.Formatter;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.widget.ViewClippingUtil;
@@ -96,6 +101,7 @@
     private final SettableWakeLock mWakeLock;
     private final DockManager mDockManager;
     private final DevicePolicyManager mDevicePolicyManager;
+    private final UserManager mUserManager;
 
     private BroadcastReceiver mBroadcastReceiver;
     private LockscreenLockIconController mLockIconController;
@@ -142,7 +148,8 @@
             DockManager dockManager,
             BroadcastDispatcher broadcastDispatcher,
             DevicePolicyManager devicePolicyManager,
-            IBatteryStats iBatteryStats) {
+            IBatteryStats iBatteryStats,
+            UserManager userManager) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mDevicePolicyManager = devicePolicyManager;
@@ -155,6 +162,7 @@
         mWakeLock = new SettableWakeLock(
                 wakeLockBuilder.setTag("Doze:KeyguardIndication").build(), TAG);
         mBatteryInfo = iBatteryStats;
+        mUserManager = userManager;
 
         mKeyguardUpdateMonitor.registerCallback(getKeyguardCallback());
         mKeyguardUpdateMonitor.registerCallback(mTickReceiver);
@@ -180,8 +188,10 @@
                     updateDisclosure();
                 }
             };
-            mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, new IntentFilter(
-                    DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED));
+            IntentFilter intentFilter = new IntentFilter();
+            intentFilter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+            intentFilter.addAction(Intent.ACTION_USER_REMOVED);
+            mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, intentFilter);
         }
     }
 
@@ -223,9 +233,8 @@
 
     private void updateDisclosure() {
         // NOTE: Because this uses IPC, avoid calling updateDisclosure() on a critical path.
-        if (whitelistIpcs(mDevicePolicyManager::isDeviceManaged)) {
-            final CharSequence organizationName =
-                    mDevicePolicyManager.getDeviceOwnerOrganizationName();
+        if (whitelistIpcs(this::isOrganizationOwnedDevice)) {
+            CharSequence organizationName = getOrganizationOwnedDeviceOrganizationName();
             if (organizationName != null) {
                 mDisclosure.switchIndication(mContext.getResources().getString(
                         R.string.do_disclosure_with_name, organizationName));
@@ -238,6 +247,38 @@
         }
     }
 
+    private boolean isOrganizationOwnedDevice() {
+        return mDevicePolicyManager.isDeviceManaged()
+                || mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile();
+    }
+
+    @Nullable
+    private CharSequence getOrganizationOwnedDeviceOrganizationName() {
+        if (mDevicePolicyManager.isDeviceManaged()) {
+            return mDevicePolicyManager.getDeviceOwnerOrganizationName();
+        } else if (mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) {
+            return getWorkProfileOrganizationName();
+        }
+        return null;
+    }
+
+    private CharSequence getWorkProfileOrganizationName() {
+        final int profileId = getWorkProfileUserId(UserHandle.myUserId());
+        if (profileId == UserHandle.USER_NULL) {
+            return null;
+        }
+        return mDevicePolicyManager.getOrganizationNameForUser(profileId);
+    }
+
+    private int getWorkProfileUserId(int userId) {
+        for (final UserInfo userInfo : mUserManager.getProfiles(userId)) {
+            if (userInfo.isManagedProfile()) {
+                return userInfo.id;
+            }
+        }
+        return UserHandle.USER_NULL;
+    }
+
     public void setVisible(boolean visible) {
         mVisible = visible;
         mIndicationArea.setVisibility(visible ? View.VISIBLE : View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 3377144..423f85f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -178,6 +178,7 @@
     private int mBucket = BUCKET_ALERTING;
     @Nullable private Long mPendingAnimationDuration;
     private boolean mIsMarkedForUserTriggeredMovement;
+    private boolean mShelfIconVisible;
 
     /**
      * @param sbn the StatusBarNotification from system server
@@ -431,6 +432,7 @@
     //TODO: This will go away when we have a way to bind an entry to a row
     public void setRow(ExpandableNotificationRow row) {
         this.row = row;
+        updateShelfIconVisibility();
     }
 
     public ExpandableNotificationRowController getRowController() {
@@ -951,6 +953,18 @@
         return mIsMarkedForUserTriggeredMovement;
     }
 
+    /** Whether or not the icon for this notification is visible in the shelf. */
+    public void setShelfIconVisible(boolean shelfIconVisible) {
+        mShelfIconVisible = shelfIconVisible;
+        updateShelfIconVisibility();
+    }
+
+    private void updateShelfIconVisibility() {
+        if (row != null) {
+            row.setShelfIconVisible(mShelfIconVisible);
+        }
+    }
+
     /**
      * Mark this entry for movement triggered by a user action (ex: changing the priorirty of a
      * conversation). This can then be used for custom animations.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 5c802bd..df1de63 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -24,6 +24,7 @@
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -116,7 +117,8 @@
             ChannelEditorDialogController channelEditorDialogController,
             CurrentUserContextTracker contextTracker,
             Provider<PriorityOnboardingDialogController.Builder> builderProvider,
-            BubbleController bubbleController) {
+            BubbleController bubbleController,
+            UiEventLogger uiEventLogger) {
         return new NotificationGutsManager(
                 context,
                 visualStabilityManager,
@@ -131,7 +133,8 @@
                 channelEditorDialogController,
                 contextTracker,
                 builderProvider,
-                bubbleController);
+                bubbleController,
+                uiEventLogger);
     }
 
     /** Provides an instance of {@link VisualStabilityManager} */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 011ad19..13f7a53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -96,13 +96,11 @@
         val shelfIcon = iconBuilder.createIconView(entry)
         shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
 
-        shelfIcon.visibility = View.INVISIBLE
         // TODO: This doesn't belong here
         shelfIcon.setOnVisibilityChangedListener { newVisibility: Int ->
-            if (entry.row != null) {
-                entry.row.setShelfIconVisible(newVisibility == View.VISIBLE)
-            }
+            entry.setShelfIconVisible(newVisibility == View.VISIBLE)
         }
+        shelfIcon.visibility = View.INVISIBLE
 
         // Construct the aod icon view.
         val aodIcon = iconBuilder.createIconView(entry)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 9a25c48..c147023 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -16,6 +16,13 @@
 
 package com.android.systemui.statusbar.notification.logging;
 
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_ALERTING;
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_FOREGROUND_SERVICE;
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_HEADS_UP;
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_MEDIA_CONTROLS;
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_PEOPLE;
+import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT;
+
 import android.annotation.Nullable;
 import android.service.notification.StatusBarNotification;
 
@@ -23,6 +30,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.nano.Notifications;
+import com.android.systemui.statusbar.notification.stack.PriorityBucket;
 
 import java.util.List;
 /**
@@ -84,7 +92,7 @@
                 if (n.getNotification() != null) {
                     proto.isGroupSummary = n.getNotification().isGroupSummary();
                 }
-                proto.section = 1 + ne.getBucket();  // We want 0 to mean not set / unknown
+                proto.section = toNotificationSection(ne.getBucket());
                 proto_array[i] = proto;
             }
             ++i;
@@ -92,4 +100,25 @@
         notificationList.notifications = proto_array;
         return notificationList;
     }
+
+    /**
+     * Maps PriorityBucket enum to Notification.SECTION constant. The two lists should generally
+     * use matching names, but the values may differ, because PriorityBucket order changes from
+     * time to time, while logs need to have stable meanings.
+     * @param bucket PriorityBucket constant
+     * @return Notification.SECTION constant
+     */
+    static int toNotificationSection(@PriorityBucket int bucket) {
+        switch(bucket) {
+            case BUCKET_MEDIA_CONTROLS : return Notifications.Notification.SECTION_MEDIA_CONTROLS;
+            case BUCKET_HEADS_UP: return Notifications.Notification.SECTION_HEADS_UP;
+            case BUCKET_FOREGROUND_SERVICE:
+                return Notifications.Notification.SECTION_FOREGROUND_SERVICE;
+            case BUCKET_PEOPLE: return Notifications.Notification.SECTION_PEOPLE;
+            case BUCKET_ALERTING: return Notifications.Notification.SECTION_ALERTING;
+            case BUCKET_SILENT: return Notifications.Notification.SECTION_SILENT;
+        }
+        return Notifications.Notification.SECTION_UNKNOWN;
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/Notifications.proto b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/Notifications.proto
index 552a5fb..c2ab275 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/Notifications.proto
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/Notifications.proto
@@ -33,13 +33,16 @@
     optional bool is_group_summary = 5;
 
     // The section of the shade that the notification is in.
-    // See NotificationSectionsManager.PriorityBucket.
+    // Sections follow NotificationSectionsManager.PriorityBucket but enum constants do not,
+    // as PriorityBucket order changes from time to time, while logs need to have stable meanings.
     enum NotificationSection {
         SECTION_UNKNOWN = 0;
         SECTION_HEADS_UP = 1;
-        SECTION_PEOPLE = 2;
-        SECTION_ALERTING = 3;
-        SECTION_SILENT = 4;
+        SECTION_MEDIA_CONTROLS = 2;
+        SECTION_PEOPLE = 3;
+        SECTION_ALERTING = 4;
+        SECTION_SILENT = 5;
+        SECTION_FOREGROUND_SERVICE = 6;
     }
     optional NotificationSection section = 6;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java
index e445c9d..28c53dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java
@@ -31,6 +31,7 @@
 import android.widget.TextView;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.R;
 
@@ -50,6 +51,7 @@
     private MetricsLogger mMetricsLogger;
     private OnSettingsClickListener mOnSettingsClickListener;
     private NotificationGuts mGutsContainer;
+    private UiEventLogger mUiEventLogger;
 
     private OnClickListener mOnOk = v -> {
         mGutsContainer.closeControls(v, false);
@@ -66,6 +68,7 @@
     public void bindGuts(final PackageManager pm,
             final OnSettingsClickListener onSettingsClick,
             final StatusBarNotification sbn,
+            final UiEventLogger uiEventLogger,
             ArraySet<Integer> activeOps) {
         mPkg = sbn.getPackageName();
         mSbn = sbn;
@@ -73,11 +76,13 @@
         mAppName = mPkg;
         mOnSettingsClickListener = onSettingsClick;
         mAppOps = activeOps;
+        mUiEventLogger = uiEventLogger;
 
         bindHeader();
         bindPrompt();
         bindButtons();
 
+        logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN);
         mMetricsLogger = new MetricsLogger();
         mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, true);
     }
@@ -188,6 +193,7 @@
 
     @Override
     public boolean handleCloseControls(boolean save, boolean force) {
+        logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_CLOSE);
         if (mMetricsLogger != null) {
             mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, false);
         }
@@ -198,4 +204,11 @@
     public int getActualHeight() {
         return getHeight();
     }
+
+    private void logUiEvent(NotificationAppOpsEvent event) {
+        if (mSbn != null) {
+            mUiEventLogger.logWithInstanceId(event,
+                    mSbn.getUid(), mSbn.getPackageName(), mSbn.getInstanceId());
+        }
+    }
 }
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 7ed8350..ccfd8a3 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
@@ -769,10 +769,6 @@
         return mContentTranslation;
     }
 
-    public boolean wantsAddAndRemoveAnimations() {
-        return true;
-    }
-
     /** Sets whether this view is the first notification in a section. */
     public void setFirstInSection(boolean firstInSection) {
         mFirstInSection = firstInSection;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationAppOpsEvent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationAppOpsEvent.java
new file mode 100644
index 0000000..c856245
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationAppOpsEvent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+enum NotificationAppOpsEvent implements UiEventLogger.UiEventEnum {
+    @UiEvent(doc = "User opened app ops controls on a notification (for active "
+            + "privacy-sensitive permissions usage)")
+    NOTIFICATION_APP_OPS_OPEN(597),
+
+    @UiEvent(doc = "User closed app ops controls")
+    NOTIFICATION_APP_OPS_CLOSE(598),
+
+    @UiEvent(doc = "User clicked through to settings in app ops controls")
+    NOTIFICATION_APP_OPS_SETTINGS_CLICK(599);
+
+    private final int mId;
+    NotificationAppOpsEvent(int id) {
+        mId = id;
+    }
+    @Override public int getId() {
+        return mId;
+    }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationControlsEvent.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationControlsEvent.java
new file mode 100644
index 0000000..6833326
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationControlsEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+
+enum NotificationControlsEvent implements UiEventLogger.UiEventEnum {
+    @UiEvent(doc = "The user opened the notification inline controls.")
+    NOTIFICATION_CONTROLS_OPEN(594),
+
+    @UiEvent(doc = "In notification inline controls, the user saved a notification channel "
+            + "importance change.")
+    NOTIFICATION_CONTROLS_SAVE_IMPORTANCE(595),
+
+    @UiEvent(doc = "The user closed the notification inline controls.")
+    NOTIFICATION_CONTROLS_CLOSE(596);
+
+    private final int mId;
+    NotificationControlsEvent(int id) {
+        mId = id;
+    }
+    @Override public int getId() {
+        return mId;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 8337cbe4..24883f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settingslib.notification.ConversationIconFactory;
 import com.android.systemui.Dependency;
@@ -121,6 +122,7 @@
     private final ShortcutManager mShortcutManager;
     private final CurrentUserContextTracker mContextTracker;
     private final Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider;
+    private final UiEventLogger mUiEventLogger;
 
     /**
      * Injected constructor. See {@link NotificationsModule}.
@@ -135,7 +137,8 @@
             ChannelEditorDialogController channelEditorDialogController,
             CurrentUserContextTracker contextTracker,
             Provider<PriorityOnboardingDialogController.Builder> builderProvider,
-            BubbleController bubbleController) {
+            BubbleController bubbleController,
+            UiEventLogger uiEventLogger) {
         mContext = context;
         mVisualStabilityManager = visualStabilityManager;
         mStatusBarLazy = statusBarLazy;
@@ -150,6 +153,7 @@
         mBuilderProvider = builderProvider;
         mChannelEditorDialogController = channelEditorDialogController;
         mBubbleController = bubbleController;
+        mUiEventLogger = uiEventLogger;
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter,
@@ -315,12 +319,16 @@
 
         AppOpsInfo.OnSettingsClickListener onSettingsClick =
                 (View v, String pkg, int uid, ArraySet<Integer> ops) -> {
-            mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
-            guts.resetFalsingCheck();
-            startAppOpsSettingsActivity(pkg, uid, ops, row);
+                    mUiEventLogger.logWithInstanceId(
+                            NotificationAppOpsEvent.NOTIFICATION_APP_OPS_SETTINGS_CLICK,
+                            sbn.getUid(), sbn.getPackageName(), sbn.getInstanceId());
+                    mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
+                    guts.resetFalsingCheck();
+                    startAppOpsSettingsActivity(pkg, uid, ops, row);
         };
         if (!row.getEntry().mActiveAppOps.isEmpty()) {
-            appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, row.getEntry().mActiveAppOps);
+            appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, mUiEventLogger,
+                    row.getEntry().mActiveAppOps);
         }
     }
 
@@ -370,6 +378,7 @@
                 row.getEntry(),
                 onSettingsClick,
                 onAppSettingsClick,
+                mUiEventLogger,
                 mDeviceProvisionedController.isDeviceProvisioned(),
                 row.getIsNonblockable(),
                 mHighPriorityProvider.isHighPriority(row.getEntry()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index a131ebe..f0c93b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -56,6 +56,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
@@ -122,6 +123,7 @@
     private OnAppSettingsClickListener mAppSettingsClickListener;
     private NotificationGuts mGutsContainer;
     private Drawable mPkgIcon;
+    private UiEventLogger mUiEventLogger;
 
     @VisibleForTesting
     boolean mSkipPost = false;
@@ -182,6 +184,7 @@
             NotificationEntry entry,
             OnSettingsClickListener onSettingsClick,
             OnAppSettingsClickListener onAppSettingsClick,
+            UiEventLogger uiEventLogger,
             boolean isDeviceProvisioned,
             boolean isNonblockable,
             boolean wasShownHighPriority)
@@ -205,6 +208,7 @@
         mAppUid = mSbn.getUid();
         mDelegatePkg = mSbn.getOpPkg();
         mIsDeviceProvisioned = isDeviceProvisioned;
+        mUiEventLogger = uiEventLogger;
 
         int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
                 pkg, mAppUid, false /* includeDeleted */);
@@ -223,6 +227,7 @@
 
         bindInlineControls();
 
+        logUiEvent(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN);
         mMetricsLogger.write(notificationControlsLogMaker());
     }
 
@@ -397,6 +402,7 @@
      */
     private void updateImportance() {
         if (mChosenImportance != null) {
+            logUiEvent(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE);
             mMetricsLogger.write(importanceChangeLogMaker());
 
             int newImportance = mChosenImportance;
@@ -483,6 +489,7 @@
 
         bindInlineControls();
 
+        logUiEvent(NotificationControlsEvent.NOTIFICATION_CONTROLS_CLOSE);
         mMetricsLogger.write(notificationControlsLogMaker().setType(MetricsEvent.TYPE_CLOSE));
     }
 
@@ -627,6 +634,13 @@
         }
     }
 
+    private void logUiEvent(NotificationControlsEvent event) {
+        if (mSbn != null) {
+            mUiEventLogger.logWithInstanceId(event,
+                    mSbn.getUid(), mSbn.getPackageName(), mSbn.getInstanceId());
+        }
+    }
+
     /**
      * Returns a LogMaker with all available notification information.
      * Caller should set category, type, and maybe subtype, before passing it to mMetricsLogger.
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 b4220f1..11e698b 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
@@ -83,6 +83,8 @@
     private float mDozeAmount = 0.0f;
     private HeadsUpManager mHeadUpManager;
     private Runnable mOnPulseHeightChangedListener;
+    private ExpandableNotificationRow mTrackedHeadsUpRow;
+    private float mAppearFraction;
 
     public AmbientState(
             Context context,
@@ -543,4 +545,27 @@
     public Runnable getOnPulseHeightChangedListener() {
         return mOnPulseHeightChangedListener;
     }
+
+    public void setTrackedHeadsUpRow(ExpandableNotificationRow row) {
+        mTrackedHeadsUpRow = row;
+    }
+
+    /**
+     * Returns the currently tracked heads up row, if there is one and it is currently above the
+     * shelf (still appearing).
+     */
+    public ExpandableNotificationRow getTrackedHeadsUpRow() {
+        if (mTrackedHeadsUpRow == null || !mTrackedHeadsUpRow.isAboveShelf()) {
+            return null;
+        }
+        return mTrackedHeadsUpRow;
+    }
+
+    public void setAppearFraction(float appearFraction) {
+        mAppearFraction = appearFraction;
+    }
+
+    public float getAppearFraction() {
+        return mAppearFraction;
+    }
 }
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 383f2a2..040f707 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
@@ -50,9 +50,4 @@
         layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
         layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
     }
-
-    @Override
-    public boolean wantsAddAndRemoveAnimations() {
-        return false;
-    }
 }
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 684bf19..b9d31a9 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
@@ -97,6 +97,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
@@ -200,6 +201,7 @@
     private final KeyguardBypassController mKeyguardBypassController;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final SysuiStatusBarStateController mStatusbarStateController;
+    private final KeyguardMediaController mKeyguardMediaController;
 
     private ExpandHelper mExpandHelper;
     private final NotificationSwipeHelper mSwipeHelper;
@@ -533,6 +535,7 @@
     private float mLastSentAppear;
     private float mLastSentExpandedHeight;
     private boolean mWillExpand;
+    private int mGapHeight;
 
     private int mWaterfallTopInset;
 
@@ -552,6 +555,7 @@
             SysuiStatusBarStateController statusBarStateController,
             HeadsUpManagerPhone headsUpManager,
             KeyguardBypassController keyguardBypassController,
+            KeyguardMediaController keyguardMediaController,
             FalsingManager falsingManager,
             NotificationLockscreenUserManager notificationLockscreenUserManager,
             NotificationGutsManager notificationGutsManager,
@@ -670,6 +674,16 @@
         initializeForegroundServiceSection(fgsFeatureController);
         mUiEventLogger = uiEventLogger;
         mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
+        mKeyguardMediaController = keyguardMediaController;
+        keyguardMediaController.setVisibilityChangedListener((visible) -> {
+            if (visible) {
+                generateAddAnimation(keyguardMediaController.getView(), false /*fromMoreCard */);
+            } else {
+                generateRemoveAnimation(keyguardMediaController.getView());
+            }
+            requestChildrenUpdate();
+            return null;
+        });
     }
 
     private void initializeForegroundServiceSection(
@@ -1047,6 +1061,7 @@
 
         Resources res = context.getResources();
         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
+        mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
         mStackScrollAlgorithm.initView(context);
         mAmbientState.reload(context);
         mPaddingBetweenElements = Math.max(1,
@@ -1407,14 +1422,12 @@
                 // start
                 translationY = height - appearStartPosition + getExpandTranslationStart();
             }
+            stackHeight = (int) (height - translationY);
             if (isHeadsUpTransition()) {
-                stackHeight =
-                        getFirstVisibleSection().getFirstVisibleChild().getPinnedHeadsUpHeight();
                 translationY = MathUtils.lerp(mHeadsUpInset - mTopPadding, 0, appearFraction);
-            } else {
-                stackHeight = (int) (height - translationY);
             }
         }
+        mAmbientState.setAppearFraction(appearFraction);
         if (stackHeight != mCurrentStackHeight) {
             mCurrentStackHeight = stackHeight;
             updateAlgorithmHeightAndPadding();
@@ -1532,17 +1545,18 @@
      */
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private float getAppearEndPosition() {
-        int appearPosition;
-        int notGoneChildCount = getNotGoneChildCount();
-        if (mEmptyShadeView.getVisibility() == GONE && notGoneChildCount != 0) {
+        int appearPosition = 0;
+        int visibleNotifCount = getVisibleNotificationCount();
+        if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) {
             if (isHeadsUpTransition()
                     || (mHeadsUpManager.hasPinnedHeadsUp() && !mAmbientState.isDozing())) {
-                appearPosition = getTopHeadsUpPinnedHeight();
-            } else {
-                appearPosition = 0;
-                if (notGoneChildCount >= 1 && mShelf.getVisibility() != GONE) {
-                    appearPosition += mShelf.getIntrinsicHeight();
+                if (mShelf.getVisibility() != GONE && visibleNotifCount > 1) {
+                    appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
                 }
+                appearPosition += getTopHeadsUpPinnedHeight()
+                        + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow());
+            } else if (mShelf.getVisibility() != GONE) {
+                appearPosition += mShelf.getIntrinsicHeight();
             }
         } else {
             appearPosition = mEmptyShadeView.getHeight();
@@ -1552,9 +1566,7 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     private boolean isHeadsUpTransition() {
-        NotificationSection firstVisibleSection = getFirstVisibleSection();
-        return mTrackingHeadsUp && firstVisibleSection != null
-                && firstVisibleSection.getFirstVisibleChild().isAboveShelf();
+        return mAmbientState.getTrackedHeadsUpRow() != null;
     }
 
     /**
@@ -2962,7 +2974,16 @@
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     public int getLayoutMinHeight() {
         if (isHeadsUpTransition()) {
-            return getTopHeadsUpPinnedHeight();
+            ExpandableNotificationRow trackedHeadsUpRow = mAmbientState.getTrackedHeadsUpRow();
+            if (trackedHeadsUpRow.isAboveShelf()) {
+                int hunDistance = (int) MathUtils.lerp(
+                        0,
+                        getPositionInLinearLayout(trackedHeadsUpRow),
+                        mAmbientState.getAppearFraction());
+                return getTopHeadsUpPinnedHeight() + hunDistance;
+            } else {
+                return getTopHeadsUpPinnedHeight();
+            }
         }
         return mShelf.getVisibility() == GONE ? 0 : mShelf.getIntrinsicHeight();
     }
@@ -3101,9 +3122,6 @@
      */
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private boolean generateRemoveAnimation(ExpandableView child) {
-        if (!child.wantsAddAndRemoveAnimations()) {
-            return false;
-        }
         if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
             mAddedHeadsUpChildren.remove(child);
             return false;
@@ -3458,8 +3476,7 @@
     @Override
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
-        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()
-                && child.wantsAddAndRemoveAnimations()) {
+        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
             // Generate Animations
             mChildrenToAddAnimated.add(child);
             if (fromMoreCard) {
@@ -3654,6 +3671,8 @@
                     ignoreChildren = false;
                 }
                 childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
+            } else if (child instanceof MediaHeaderView) {
+                childWasSwipedOut = true;
             }
             if (!childWasSwipedOut) {
                 Rect clipBounds = child.getClipBounds();
@@ -5082,8 +5101,10 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
-    public int getFooterViewHeight() {
-        return mFooterView == null ? 0 : mFooterView.getHeight() + mPaddingBetweenElements;
+    public int getFooterViewHeightWithPadding() {
+        return mFooterView == null ? 0 : mFooterView.getHeight()
+                + mPaddingBetweenElements
+                + mGapHeight;
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -5284,6 +5305,7 @@
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
+        mAmbientState.setTrackedHeadsUpRow(row);
         mTrackingHeadsUp = row != null;
         mRoundnessManager.setTrackingHeadsUp(row);
     }
@@ -6370,7 +6392,7 @@
         @Override
         public void onDragCancelled(View v) {
             setSwipingInProgress(false);
-            mFalsingManager.onNotificatonStopDismissing();
+            mFalsingManager.onNotificationStopDismissing();
         }
 
         /**
@@ -6470,7 +6492,7 @@
 
         @Override
         public void onBeginDrag(View v) {
-            mFalsingManager.onNotificatonStartDismissing();
+            mFalsingManager.onNotificationStartDismissing();
             setSwipingInProgress(true);
             mAmbientState.onBeginDrag((ExpandableView) v);
             updateContinuousShadowDrawing();
@@ -6629,7 +6651,9 @@
         /* Only ever called as a consequence of a lockscreen expansion gesture. */
         @Override
         public boolean onDraggedDown(View startingChild, int dragLengthY) {
-            if (mStatusBarState == StatusBarState.KEYGUARD && hasActiveNotifications()) {
+            boolean canDragDown = hasActiveNotifications()
+                    || mKeyguardMediaController.getView().getVisibility() == VISIBLE;
+            if (mStatusBarState == StatusBarState.KEYGUARD && canDragDown) {
                 mLockscreenGestureLogger.write(
                         MetricsEvent.ACTION_LS_SHADE,
                         (int) (dragLengthY / mDisplayMetrics.density),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index a4598e9..541c784 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.util.Log;
+import android.util.MathUtils;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -441,7 +442,7 @@
         } else if (isEmptyShadeView) {
             childViewState.yTranslation = ambientState.getInnerHeight() - childHeight
                     + ambientState.getStackTranslation() * 0.25f;
-        } else {
+        } else if (child != ambientState.getTrackedHeadsUpRow()) {
             clampPositionToShelf(child, childViewState, ambientState);
         }
 
@@ -539,6 +540,19 @@
     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
             AmbientState ambientState) {
         int childCount = algorithmState.visibleChildren.size();
+
+        // Move the tracked heads up into position during the appear animation, by interpolating
+        // between the HUN inset (where it will appear as a HUN) and the end position in the shade
+        ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
+        if (trackedHeadsUpRow != null) {
+            ExpandableViewState childState = trackedHeadsUpRow.getViewState();
+            if (childState != null) {
+                float endPosition = childState.yTranslation - ambientState.getStackTranslation();
+                childState.yTranslation = MathUtils.lerp(
+                        mHeadsUpInset, endPosition, ambientState.getAppearFraction());
+            }
+        }
+
         ExpandableNotificationRow topHeadsUpEntry = null;
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
@@ -561,7 +575,7 @@
                         && !row.showingPulsing()) {
                     // Ensure that the heads up is always visible even when scrolled off
                     clampHunToTop(ambientState, row, childState);
-                    if (i == 0 && row.isAboveShelf()) {
+                    if (isTopEntry && row.isAboveShelf()) {
                         // the first hun can't get off screen.
                         clampHunToMaxTranslation(ambientState, row, childState);
                         childState.hidden = false;
@@ -636,9 +650,13 @@
             return;
         }
 
+        ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
+        boolean isBeforeTrackedHeadsUp = trackedHeadsUpRow != null
+                && mHostView.indexOfChild(child) < mHostView.indexOfChild(trackedHeadsUpRow);
+
         int shelfStart = ambientState.getInnerHeight()
                 - ambientState.getShelf().getIntrinsicHeight();
-        if (ambientState.isAppearing() && !child.isAboveShelf()) {
+        if (ambientState.isAppearing() && !child.isAboveShelf() && !isBeforeTrackedHeadsUp) {
             // Don't show none heads-up notifications while in appearing phase.
             childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart);
         }
@@ -695,7 +713,8 @@
             }
             childViewState.zTranslation = baseZ
                     + childrenOnTop * zDistanceBetweenElements;
-        } else if (i == 0 && (child.isAboveShelf() || child.showingPulsing())) {
+        } else if (child == ambientState.getTrackedHeadsUpRow()
+                || (i == 0 && (child.isAboveShelf() || child.showingPulsing()))) {
             // In case this is a new view that has never been measured before, we don't want to
             // elevate if we are currently expanded more then the notification
             int shelfHeight = ambientState.getShelf() == null ? 0 :
@@ -703,7 +722,7 @@
             float shelfStart = ambientState.getInnerHeight()
                     - shelfHeight + ambientState.getTopPadding()
                     + ambientState.getStackTranslation();
-            float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight()
+            float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight()
                     + mPaddingBetweenElements;
             if (shelfStart > notificationEnd) {
                 childViewState.zTranslation = baseZ;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 90548ba..063305e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -221,7 +221,7 @@
      */
     private VerticalNavigationHandle mOrientationHandle;
     private WindowManager.LayoutParams mOrientationParams;
-    private int mStartingQuickSwitchRotation;
+    private int mStartingQuickSwitchRotation = -1;
     private int mCurrentRotation;
     private ViewTreeObserver.OnGlobalLayoutListener mOrientationHandleGlobalLayoutListener;
     private UiEventLogger mUiEventLogger;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 06484a2..dbff643 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -74,6 +74,7 @@
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
@@ -372,6 +373,11 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (isGesturalMode(mNavBarMode) && mImeVisible
+                && event.getAction() == MotionEvent.ACTION_DOWN) {
+            SysUiStatsLog.write(SysUiStatsLog.IME_TOUCH_REPORTED,
+                    (int) event.getX(), (int) event.getY());
+        }
         return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index e720d82..0b664e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -2397,8 +2397,8 @@
     }
 
     @Override
-    protected int getClearAllHeight() {
-        return mNotificationStackScroller.getFooterViewHeight();
+    protected int getClearAllHeightWithPadding() {
+        return mNotificationStackScroller.getFooterViewHeightWithPadding();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index a902e1b..caddc4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -537,9 +537,9 @@
         // the animation only to the last notification, and then jump to the maximum panel height so
         // clear all just fades in and the decelerating motion is towards the last notification.
         final boolean clearAllExpandHack = expand &&
-                shouldExpandToTopOfClearAll(getMaxPanelHeight() - getClearAllHeight());
+                shouldExpandToTopOfClearAll(getMaxPanelHeight() - getClearAllHeightWithPadding());
         if (clearAllExpandHack) {
-            target = getMaxPanelHeight() - getClearAllHeight();
+            target = getMaxPanelHeight() - getClearAllHeightWithPadding();
         }
         if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
             notifyExpandingFinished();
@@ -1030,9 +1030,9 @@
     protected abstract boolean isClearAllVisible();
 
     /**
-     * @return the height of the clear all button, in pixels
+     * @return the height of the clear all button, in pixels including padding
      */
-    protected abstract int getClearAllHeight();
+    protected abstract int getClearAllHeightWithPadding();
 
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         mHeadsUpManager = headsUpManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index 3df1c11..6bc0565 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -72,9 +72,6 @@
         } catch (RemoteException ex) {
             // If the system process isn't there we're doomed anyway.
         }
-
-        // Creating AudioRecordingDisclosureBar and just letting it run
-        new AudioRecordingDisclosureBar(mContext);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
index b73aeb3..5143e42 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -46,6 +46,9 @@
     private var state = TransitionViewState()
     private var pivot = PointF()
     private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+    private var currentHeight: Int = 0
+    private var currentWidth: Int = 0
+    var sizeChangedListener: ((Int, Int) -> Unit)? = null
 
     init {
         animator.apply {
@@ -67,7 +70,16 @@
                 progress = animator.animatedFraction,
                 pivot = pivot,
                 resultState = currentState)
-        view.setState(currentState)
+        applyStateToLayout(currentState)
+    }
+
+    private fun applyStateToLayout(state: TransitionViewState) {
+        transitionLayout?.setState(state)
+        if (currentHeight != state.height || currentWidth != state.width) {
+            currentHeight = state.height
+            currentWidth = state.width
+            sizeChangedListener?.invoke(currentWidth, currentHeight)
+        }
     }
 
     /**
@@ -213,7 +225,7 @@
         this.state = state.copy()
         if (applyImmediately || transitionLayout == null) {
             animator.cancel()
-            transitionLayout?.setState(this.state)
+            applyStateToLayout(this.state)
             currentState = state.copy(reusedState = currentState)
         } else if (animated) {
             animationStartState = currentState.copy()
@@ -221,7 +233,7 @@
             animator.startDelay = delay
             animator.start()
         } else if (!animator.isRunning) {
-            transitionLayout?.setState(this.state)
+            applyStateToLayout(this.state)
             currentState = state.copy(reusedState = currentState)
         }
         // otherwise the desired state was updated and the animation will go to the new target
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 5b6444d..d6e7a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
@@ -70,7 +70,10 @@
     }
 
     override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
-        if (child?.measuredWidth == 0 || measuredWidth == 0 || child?.requiresRemeasuring == true) {
+        if (child == null) {
+            throw IllegalArgumentException("child must be non-null")
+        }
+        if (child.measuredWidth == 0 || measuredWidth == 0 || child.requiresRemeasuring == true) {
             super.addView(child, index, params)
             return
         }
@@ -78,11 +81,13 @@
         // right size when being attached to this view
         invalidate()
         addViewInLayout(child, index, params, true /* preventRequestLayout */)
+        // RTL properties are normally resolved in onMeasure(), which we are intentionally skipping
+        child.resolveRtlPropertiesIfNeeded()
         val left = paddingLeft
         val top = paddingTop
         val paddingHorizontal = paddingStart + paddingEnd
         val paddingVertical = paddingTop + paddingBottom
-        child!!.layout(left,
+        child.layout(left,
                 top,
                 left + measuredWidth - paddingHorizontal,
                 top + measuredHeight - paddingVertical)
diff --git a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
index 7b11452..8ba5b99 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java
@@ -56,12 +56,14 @@
 
     private static final boolean DEBUG = false;
 
+    // NOTE: All these constants came from InsetsController.
     public static final int ANIMATION_DURATION_SHOW_MS = 275;
     public static final int ANIMATION_DURATION_HIDE_MS = 340;
     public static final Interpolator INTERPOLATOR = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
     private static final int DIRECTION_NONE = 0;
     private static final int DIRECTION_SHOW = 1;
     private static final int DIRECTION_HIDE = 2;
+    private static final int FLOATING_IME_BOTTOM_INSET = -80;
 
     SystemWindows mSystemWindows;
     final Handler mHandler;
@@ -271,8 +273,16 @@
             }
             // Set frame, but only if the new frame isn't empty -- this maintains continuity
             final Rect newFrame = imeSource.getFrame();
-            if (newFrame.height() != 0) {
+            mImeFrame.set(newFrame);
+            final boolean isFloating = newFrame.height() == 0;
+            if (isFloating) {
+                // This is likely a "floating" or "expanded" IME, so to get animations, just
+                // pretend the ime has some size just below the screen.
                 mImeFrame.set(newFrame);
+                final int floatingInset = (int) (
+                        mSystemWindows.mDisplayController.getDisplayLayout(mDisplayId).density()
+                                * FLOATING_IME_BOTTOM_INSET);
+                mImeFrame.bottom -= floatingInset;
             }
             if (DEBUG) {
                 Slog.d(TAG, "Run startAnim  show:" + show + "  was:"
@@ -316,6 +326,8 @@
                 SurfaceControl.Transaction t = mTransactionPool.acquire();
                 float value = (float) animation.getAnimatedValue();
                 t.setPosition(mImeSourceControl.getLeash(), x, value);
+                final float alpha = isFloating ? (value - hiddenY) / (shownY - hiddenY) : 1.f;
+                t.setAlpha(mImeSourceControl.getLeash(), alpha);
                 dispatchPositionChanged(mDisplayId, imeTop(value), t);
                 t.apply();
                 mTransactionPool.release(t);
@@ -327,6 +339,8 @@
                 public void onAnimationStart(Animator animation) {
                     SurfaceControl.Transaction t = mTransactionPool.acquire();
                     t.setPosition(mImeSourceControl.getLeash(), x, startY);
+                    final float alpha = isFloating ? (startY - hiddenY) / (shownY - hiddenY) : 1.f;
+                    t.setAlpha(mImeSourceControl.getLeash(), alpha);
                     if (DEBUG) {
                         Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:"
                                 + imeTop(hiddenY) + "->" + imeTop(shownY)
@@ -351,6 +365,7 @@
                     SurfaceControl.Transaction t = mTransactionPool.acquire();
                     if (!mCancelled) {
                         t.setPosition(mImeSourceControl.getLeash(), x, endY);
+                        t.setAlpha(mImeSourceControl.getLeash(), 1.f);
                     }
                     dispatchEndPositioning(mDisplayId, mCancelled, t);
                     if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
index 9aee11e..008dc12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.MediaHeaderView
 import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.util.mockito.capture
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -94,7 +95,7 @@
 
     private fun triggerVisibilityListener() {
         keyguardMediaController.attach(mediaHeaderView)
-        verify(mediaHost).visibleChangedListener = visibilityListener.capture()
+        verify(mediaHost).addVisibilityChangeListener(capture(visibilityListener))
         visibilityListener.value.invoke(true)
     }
 }
\ No newline at end of file
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 a297f32..b7f317b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -242,4 +242,15 @@
         assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
                 com.android.internal.R.string.ext_media_seamless_action))
     }
+
+    @Test
+    fun bindDeviceResumptionPlayer() {
+        player.attach(holder)
+        val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
+                emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null,
+                resumption = true)
+        player.bind(state)
+        assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
+        assertThat(seamless.isEnabled()).isFalse()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
index 618ee89..9fdd9ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -79,8 +79,8 @@
         mManager.addListener(mListener);
 
         mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
-                new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, KEY,
-                false);
+                new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false,
+                KEY, false);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 54520be..20a6da5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -1,6 +1,11 @@
 package com.android.systemui.media
 
-import android.app.Notification
+import android.app.Notification.MediaStyle
+import android.app.PendingIntent
+import android.media.MediaDescription
+import android.media.MediaMetadata
+import android.media.session.MediaController
+import android.media.session.MediaSession
 import android.service.notification.StatusBarNotification
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
@@ -8,6 +13,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.SbnBuilder
+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
@@ -18,12 +26,14 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-import java.util.concurrent.Executor
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "KEY"
 private const val PACKAGE_NAME = "com.android.systemui"
+private const val APP_NAME = "SystemUI"
+private const val SESSION_ARTIST = "artist"
+private const val SESSION_TITLE = "title"
 
 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
 private fun <T> anyObject(): T {
@@ -36,33 +46,47 @@
 class MediaDataManagerTest : SysuiTestCase() {
 
     @Mock lateinit var mediaControllerFactory: MediaControllerFactory
-    @Mock lateinit var backgroundExecutor: Executor
-    @Mock lateinit var foregroundExecutor: Executor
+    @Mock lateinit var controller: MediaController
+    lateinit var session: MediaSession
+    lateinit var metadataBuilder: MediaMetadata.Builder
+    lateinit var backgroundExecutor: FakeExecutor
+    lateinit var foregroundExecutor: FakeExecutor
     @Mock lateinit var dumpManager: DumpManager
     @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
     @Mock lateinit var mediaResumeListener: MediaResumeListener
+    @Mock lateinit var pendingIntent: PendingIntent
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     lateinit var mediaDataManager: MediaDataManager
     lateinit var mediaNotification: StatusBarNotification
 
     @Before
     fun setup() {
+        foregroundExecutor = FakeExecutor(FakeSystemClock())
+        backgroundExecutor = FakeExecutor(FakeSystemClock())
         mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor,
                 mediaControllerFactory, broadcastDispatcher, dumpManager,
                 mediaTimeoutListener, mediaResumeListener, useMediaResumption = true,
                 useQsMediaPlayer = true)
-        val sbn = mock(StatusBarNotification::class.java)
-        val notification = mock(Notification::class.java)
-        whenever(notification.hasMediaSession()).thenReturn(true)
-        whenever(notification.notificationStyle).thenReturn(Notification.MediaStyle::class.java)
-        whenever(sbn.notification).thenReturn(notification)
-        whenever(sbn.packageName).thenReturn(PACKAGE_NAME)
-        mediaNotification = sbn
+        session = MediaSession(context, "MediaDataManagerTestSession")
+        mediaNotification = SbnBuilder().run {
+            setPkg(PACKAGE_NAME)
+            modifyNotification(context).also {
+                it.setSmallIcon(android.R.drawable.ic_media_pause)
+                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+            }
+            build()
+        }
+        metadataBuilder = MediaMetadata.Builder().apply {
+            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+        }
+        whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
     }
 
     @After
     fun tearDown() {
+        session.release()
         mediaDataManager.destroy()
     }
 
@@ -82,7 +106,7 @@
     @Test
     fun testLoadsMetadataOnBackground() {
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-        verify(backgroundExecutor).execute(anyObject())
+        assertThat(backgroundExecutor.numPending()).isEqualTo(1)
     }
 
     @Test
@@ -123,4 +147,66 @@
 
         verify(listener).onMediaDataRemoved(eq(KEY))
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testOnNotificationRemoved_withResumption() {
+        // GIVEN that the manager has a notification with a resume action
+        val listener = TestListener()
+        mediaDataManager.addListener(listener)
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        val data = listener.data!!
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+        // WHEN the notification is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+        // THEN the media data indicates that it is
+        assertThat(listener.data!!.resumption).isTrue()
+    }
+
+    @Test
+    fun testAddResumptionControls() {
+        val listener = TestListener()
+        mediaDataManager.addListener(listener)
+        // WHEN resumption controls are added`
+        val desc = MediaDescription.Builder().run {
+            setTitle(SESSION_TITLE)
+            build()
+        }
+        mediaDataManager.addResumptionControls(desc, Runnable {}, session.sessionToken, APP_NAME,
+                pendingIntent, PACKAGE_NAME)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        // THEN the media data indicates that it is for resumption
+        val data = listener.data!!
+        assertThat(data.resumption).isTrue()
+        assertThat(data.song).isEqualTo(SESSION_TITLE)
+        assertThat(data.app).isEqualTo(APP_NAME)
+        assertThat(data.actions).hasSize(1)
+    }
+
+    /**
+     * Simple implementation of [MediaDataManager.Listener] for the test.
+     *
+     * Giving up on trying to get a mock Listener and ArgumentCaptor to work.
+     */
+    private class TestListener : MediaDataManager.Listener {
+        var data: MediaData? = null
+        var key: String? = null
+        var oldKey: String? = null
+
+        override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+            this.key = key
+            this.oldKey = oldKey
+            this.data = data
+        }
+
+        override fun onMediaDataRemoved(key: String) {
+            this.key = key
+            oldKey = null
+            data = null
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
index c9e6f55..91c5ff8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -70,7 +70,7 @@
     @Mock
     private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
     @Mock
-    private lateinit var mediaViewManager: MediaViewManager
+    private lateinit var mediaCarouselController: MediaCarouselController
     @Mock
     private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
     @Captor
@@ -82,13 +82,13 @@
 
     @Before
     fun setup() {
-        `when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
+        `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
         mediaHiearchyManager = MediaHierarchyManager(
                 context,
                 statusBarStateController,
                 keyguardStateController,
                 bypassController,
-                mediaViewManager,
+                mediaCarouselController,
                 notificationLockscreenUserManager,
                 wakefulnessLifecycle)
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
@@ -97,7 +97,7 @@
         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)
+        clearInvocations(mediaCarouselController)
 
     }
 
@@ -118,14 +118,14 @@
     fun testBlockedWhenScreenTurningOff() {
         // Let's set it onto QS:
         mediaHiearchyManager.qsExpansion = 1.0f
-        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+        verify(mediaCarouselController).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)
+        clearInvocations(mediaCarouselController)
         mediaHiearchyManager.qsExpansion = 0.0f
-        verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+        verify(mediaCarouselController, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
     }
 
@@ -133,13 +133,13 @@
     fun testAllowedWhenNotTurningOff() {
         // Let's set it onto QS:
         mediaHiearchyManager.qsExpansion = 1.0f
-        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
-        clearInvocations(mediaViewManager)
+        clearInvocations(mediaCarouselController)
         mediaHiearchyManager.qsExpansion = 0.0f
-        verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+        verify(mediaCarouselController).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/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 22f50d0..e0d2681 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar;
 
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
@@ -40,6 +42,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.UserInfo;
 import android.graphics.Color;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.face.FaceManager;
@@ -79,6 +82,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Collections;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class KeyguardIndicationControllerTest extends SysuiTestCase {
@@ -154,7 +159,8 @@
 
         mController = new KeyguardIndicationController(mContext, mWakeLockBuilder,
                 mKeyguardStateController, mStatusBarStateController, mKeyguardUpdateMonitor,
-                mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats);
+                mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats,
+                mUserManager);
         mController.setIndicationArea(mIndicationArea);
         mController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
         clearInvocations(mIBatteryStats);
@@ -242,6 +248,7 @@
     @Test
     public void disclosure_unmanaged() {
         when(mDevicePolicyManager.isDeviceManaged()).thenReturn(false);
+        when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(false);
         createController();
 
         verify(mDisclosure).setVisibility(View.GONE);
@@ -249,7 +256,7 @@
     }
 
     @Test
-    public void disclosure_managedNoOwnerName() {
+    public void disclosure_deviceOwner_noOwnerName() {
         when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true);
         when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(null);
         createController();
@@ -260,6 +267,19 @@
     }
 
     @Test
+    public void disclosure_orgOwnedDeviceWithManagedProfile_noOwnerName() {
+        when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(true);
+        when(mUserManager.getProfiles(anyInt())).thenReturn(Collections.singletonList(
+                new UserInfo(10, /* name */ null, /* flags */ FLAG_MANAGED_PROFILE)));
+        when(mDevicePolicyManager.getOrganizationNameForUser(eq(10))).thenReturn(null);
+        createController();
+
+        verify(mDisclosure).setVisibility(View.VISIBLE);
+        verify(mDisclosure).switchIndication(R.string.do_disclosure_generic);
+        verifyNoMoreInteractions(mDisclosure);
+    }
+
+    @Test
     public void disclosure_hiddenWhenDozing() {
         when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true);
         when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(null);
@@ -292,7 +312,7 @@
     }
 
     @Test
-    public void disclosure_managedOwnerName() {
+    public void disclosure_deviceOwner_withOwnerName() {
         when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true);
         when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(ORGANIZATION_NAME);
         createController();
@@ -303,6 +323,19 @@
     }
 
     @Test
+    public void disclosure_orgOwnedDeviceWithManagedProfile_withOwnerName() {
+        when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(true);
+        when(mUserManager.getProfiles(anyInt())).thenReturn(Collections.singletonList(
+                new UserInfo(10, /* name */ null, FLAG_MANAGED_PROFILE)));
+        when(mDevicePolicyManager.getOrganizationNameForUser(eq(10))).thenReturn(ORGANIZATION_NAME);
+        createController();
+
+        verify(mDisclosure).setVisibility(View.VISIBLE);
+        verify(mDisclosure).switchIndication(mDisclosureWithOrganization);
+        verifyNoMoreInteractions(mDisclosure);
+    }
+
+    @Test
     public void disclosure_updateOnTheFly() {
         ArgumentCaptor<BroadcastReceiver> receiver = ArgumentCaptor.forClass(
                 BroadcastReceiver.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
index 06bad80..c979dc6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.notification.logging;
 
-import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_ALERTING;
-
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -200,7 +198,7 @@
         assertEquals(TEST_UID, n.uid);
         assertEquals(1, n.instanceId);
         assertFalse(n.isGroupSummary);
-        assertEquals(1 + BUCKET_ALERTING, n.section);
+        assertEquals(Notifications.Notification.SECTION_ALERTING, n.section);
     }
 
     @Test
@@ -217,7 +215,7 @@
         assertEquals(TEST_UID, n.uid);
         assertEquals(1, n.instanceId);
         assertFalse(n.isGroupSummary);
-        assertEquals(1 + BUCKET_ALERTING, n.section);
+        assertEquals(Notifications.Notification.SECTION_ALERTING, n.section);
     }
 
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java
index ec73a75..43d8b50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java
@@ -49,6 +49,7 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 
@@ -69,6 +70,7 @@
     private final PackageManager mMockPackageManager = mock(PackageManager.class);
     private final NotificationGuts mGutsParent = mock(NotificationGuts.class);
     private StatusBarNotification mSbn;
+    private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
 
     @Before
     public void setUp() throws Exception {
@@ -94,7 +96,7 @@
     @Test
     public void testBindNotification_SetsTextApplicationName() {
         when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name");
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, new ArraySet<>());
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
         final TextView textView = mAppOpsInfo.findViewById(R.id.pkgname);
         assertTrue(textView.getText().toString().contains("App Name"));
     }
@@ -104,7 +106,7 @@
         final Drawable iconDrawable = mock(Drawable.class);
         when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class)))
                 .thenReturn(iconDrawable);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, new ArraySet<>());
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
         final ImageView iconView = mAppOpsInfo.findViewById(R.id.pkgicon);
         assertEquals(iconDrawable, iconView.getDrawable());
     }
@@ -120,7 +122,7 @@
             assertEquals(expectedOps, ops);
             assertEquals(TEST_UID, uid);
             latch.countDown();
-        }, mSbn, expectedOps);
+        }, mSbn, mUiEventLogger, expectedOps);
 
         final View settingsButton = mAppOpsInfo.findViewById(R.id.settings);
         settingsButton.performClick();
@@ -129,6 +131,14 @@
     }
 
     @Test
+    public void testBindNotification_LogsOpen() throws Exception {
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
+        assertEquals(1, mUiEventLogger.numLogs());
+        assertEquals(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN.getId(),
+                mUiEventLogger.eventId(0));
+    }
+
+    @Test
     public void testOk() {
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_CAMERA);
@@ -139,7 +149,7 @@
             assertEquals(expectedOps, ops);
             assertEquals(TEST_UID, uid);
             latch.countDown();
-        }, mSbn, expectedOps);
+        }, mSbn, mUiEventLogger, expectedOps);
 
         final View okButton = mAppOpsInfo.findViewById(R.id.ok);
         okButton.performClick();
@@ -151,7 +161,7 @@
     public void testPrompt_camera() {
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_CAMERA);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is using the camera.", prompt.getText());
     }
@@ -160,7 +170,7 @@
     public void testPrompt_mic() {
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_RECORD_AUDIO);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is using the microphone.", prompt.getText());
     }
@@ -169,7 +179,7 @@
     public void testPrompt_overlay() {
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is displaying over other apps on your screen.", prompt.getText());
     }
@@ -179,7 +189,7 @@
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_CAMERA);
         expectedOps.add(OP_RECORD_AUDIO);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is using the microphone and camera.", prompt.getText());
     }
@@ -190,7 +200,7 @@
         expectedOps.add(OP_CAMERA);
         expectedOps.add(OP_RECORD_AUDIO);
         expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is displaying over other apps on your screen and using"
                 + " the microphone and camera.", prompt.getText());
@@ -201,7 +211,7 @@
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_CAMERA);
         expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is displaying over other apps on your screen and using"
                 + " the camera.", prompt.getText());
@@ -212,7 +222,7 @@
         ArraySet<Integer> expectedOps = new ArraySet<>();
         expectedOps.add(OP_RECORD_AUDIO);
         expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, expectedOps);
+        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
         TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
         assertEquals("This app is displaying over other apps on your screen and using"
                 + " the microphone.", prompt.getText());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 5b5873b..9dee843 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -64,6 +64,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -149,7 +151,8 @@
         mGutsManager = new NotificationGutsManager(mContext, mVisualStabilityManager,
                 () -> mStatusBar, mHandler, mHandler, mAccessibilityManager, mHighPriorityProvider,
                 mINotificationManager, mLauncherApps, mShortcutManager,
-                mChannelEditorDialogController, mContextTracker, mProvider, mBubbleController);
+                mChannelEditorDialogController, mContextTracker, mProvider, mBubbleController,
+                new UiEventLoggerFake());
         mGutsManager.setUpWithPresenter(mPresenter, mStackScroller,
                 mCheckSaveListener, mOnSettingsClickListener);
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
@@ -362,6 +365,7 @@
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
+                any(UiEventLogger.class),
                 eq(false),
                 eq(false),
                 eq(true) /* wasShownHighPriority */);
@@ -394,6 +398,7 @@
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
+                any(UiEventLogger.class),
                 eq(true),
                 eq(false),
                 eq(false) /* wasShownHighPriority */);
@@ -424,6 +429,7 @@
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
+                any(UiEventLogger.class),
                 eq(false),
                 eq(false),
                 eq(false) /* wasShownHighPriority */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index 6bf6072..ed982ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -62,6 +62,7 @@
 import android.widget.TextView;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -102,6 +103,7 @@
     private Set<NotificationChannel> mDefaultNotificationChannelSet = new HashSet<>();
     private StatusBarNotification mSbn;
     private NotificationEntry mEntry;
+    private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
 
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
@@ -187,6 +189,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -211,6 +214,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -231,6 +235,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -260,6 +265,7 @@
                 entry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -281,6 +287,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -307,6 +314,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -328,6 +336,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -348,6 +357,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -372,6 +382,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -392,6 +403,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 true,
                 true);
@@ -416,6 +428,7 @@
                     latch.countDown();
                 },
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -439,6 +452,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -462,6 +476,7 @@
                     assertEquals(mNotificationChannel, c);
                 },
                 null,
+                mUiEventLogger,
                 false,
                 false,
                 true);
@@ -482,6 +497,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -496,6 +512,7 @@
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> { },
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -519,6 +536,7 @@
                     latch.countDown();
                 },
                 null,
+                mUiEventLogger,
                 true,
                 true,
                 true);
@@ -543,6 +561,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -565,6 +584,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -587,6 +607,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 true,
                 true);
@@ -611,6 +632,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -630,6 +652,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -649,6 +672,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -658,6 +682,28 @@
     }
 
     @Test
+    public void testBindNotification_LogsOpen() throws Exception {
+        mNotificationInfo.bindNotification(
+                mMockPackageManager,
+                mMockINotificationManager,
+                mVisualStabilityManager,
+                mChannelEditorDialogController,
+                TEST_PACKAGE_NAME,
+                mNotificationChannel,
+                mNotificationChannelSet,
+                mEntry,
+                null,
+                null,
+                mUiEventLogger,
+                true,
+                false,
+                true);
+        assertEquals(1, mUiEventLogger.numLogs());
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
+                mUiEventLogger.eventId(0));
+    }
+
+    @Test
     public void testDoesNotUpdateNotificationChannelAfterImportanceChanged() throws Exception {
         mNotificationChannel.setImportance(IMPORTANCE_LOW);
         mNotificationInfo.bindNotification(
@@ -671,6 +717,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -696,6 +743,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -721,6 +769,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -730,6 +779,13 @@
         verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
                 anyString(), eq(TEST_UID), any());
         assertEquals(originalImportance, mNotificationChannel.getImportance());
+
+        assertEquals(2, mUiEventLogger.numLogs());
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
+                mUiEventLogger.eventId(0));
+        // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
+                mUiEventLogger.eventId(1));
     }
 
     @Test
@@ -747,6 +803,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -773,6 +830,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -789,6 +847,12 @@
         assertTrue((updated.getValue().getUserLockedFields()
                 & USER_LOCKED_IMPORTANCE) != 0);
         assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance());
+
+        assertEquals(2, mUiEventLogger.numLogs());
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
+                mUiEventLogger.eventId(0));
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
+                mUiEventLogger.eventId(1));
     }
 
     @Test
@@ -805,6 +869,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -838,6 +903,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -871,6 +937,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -907,6 +974,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -942,6 +1010,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 true);
@@ -968,6 +1037,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -997,6 +1067,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -1029,6 +1100,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false);
@@ -1040,6 +1112,10 @@
         mTestableLooper.processAllMessages();
         verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
                 eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
+
+        assertEquals(1, mUiEventLogger.numLogs());
+        assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
+                mUiEventLogger.eventId(0));
     }
 
     @Test
@@ -1056,6 +1132,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false
@@ -1088,6 +1165,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false
@@ -1113,6 +1191,7 @@
                 mEntry,
                 null,
                 null,
+                mUiEventLogger,
                 true,
                 false,
                 false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index c4bd1b2..b286f94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -53,6 +53,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.FeatureFlags;
@@ -133,6 +134,7 @@
     @Mock private MetricsLogger mMetricsLogger;
     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
     @Mock private KeyguardBypassController mKeyguardBypassController;
+    @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private ZenModeController mZenModeController;
     @Mock private NotificationSectionsManager mNotificationSectionsManager;
     @Mock private NotificationSection mNotificationSection;
@@ -209,6 +211,7 @@
                 mock(SysuiStatusBarStateController.class),
                 mHeadsUpManager,
                 mKeyguardBypassController,
+                mKeyguardMediaController,
                 new FalsingManagerFake(),
                 mLockscreenUserManager,
                 mock(NotificationGutsManager.class),
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3114a6a..2b9ce2f 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1005,6 +1005,10 @@
                 || disableDuration > 0) {
             // Response is "empty" from an UI point of view, need to notify client.
             notifyUnavailableToClient(sessionFinishedState, /* autofillableIds= */ null);
+            synchronized (mLock) {
+                mInlineSessionController.setInlineFillUiLocked(
+                        InlineFillUi.emptyUi(mCurrentViewId));
+            }
         }
 
         if (requestLog != null) {
@@ -2979,8 +2983,8 @@
                 inlineSuggestionsRequest.get(), response, focusedId, filterText,
                 /*uiCallback*/this, /*onErrorCallback*/ () -> {
                     synchronized (mLock) {
-                        mInlineSessionController.hideInlineSuggestionsUiLocked(
-                                focusedId);
+                        mInlineSessionController.setInlineFillUiLocked(
+                                InlineFillUi.emptyUi(focusedId));
                     }
                 }, remoteRenderService);
         return mInlineSessionController.setInlineFillUiLocked(inlineFillUi);
@@ -3166,12 +3170,15 @@
             notifyUnavailableToClient(AutofillManager.STATE_FINISHED, autofillableIds);
             removeSelf();
         } else {
-            if (sVerbose) {
-                if ((flags & FLAG_PASSWORD_INPUT_TYPE) != 0) {
+            if ((flags & FLAG_PASSWORD_INPUT_TYPE) != 0) {
+                if (sVerbose) {
                     Slog.v(TAG, "keeping session " + id + " when service returned null and "
                             + "augmented service is disabled for password fields. "
                             + "AutofillableIds: " + autofillableIds);
-                } else {
+                }
+                mInlineSessionController.hideInlineSuggestionsUiLocked(mCurrentViewId);
+            } else {
+                if (sVerbose) {
                     Slog.v(TAG, "keeping session " + id + " when service returned null but "
                             + "it can be augmented. AutofillableIds: " + autofillableIds);
                 }
@@ -3197,7 +3204,7 @@
     // non-null response but without datasets (for example, just SaveInfo)
     @GuardedBy("mLock")
     private Runnable triggerAugmentedAutofillLocked(int flags) {
-        // (TODO: b/141703197) Fix later by passing info to service.
+        // TODO: (b/141703197) Fix later by passing info to service.
         if ((flags & FLAG_PASSWORD_INPUT_TYPE) != 0) {
             return null;
         }
@@ -3242,7 +3249,7 @@
                         + ComponentName.flattenToShortString(mComponentName) + " not whitelisted ");
             }
             logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(),
-                    mCurrentViewId, isWhitelisted, /*isInline*/null);
+                    mCurrentViewId, isWhitelisted, /* isInline= */ null);
             return null;
         }
 
@@ -3284,6 +3291,10 @@
                                 /*onErrorCallback=*/ () -> {
                                     synchronized (mLock) {
                                         cancelAugmentedAutofillLocked();
+
+                                        // Also cancel augmented in IME
+                                        mInlineSessionController.setInlineFillUiLocked(
+                                                InlineFillUi.emptyUi(mCurrentViewId));
                                     }
                                 }, mService.getRemoteInlineSuggestionRenderServiceLocked());
                     }
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 507e983..1c31166 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -33,6 +33,7 @@
 import android.metrics.LogMaker;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.UserHandle;
 import android.service.autofill.BatchUpdates;
 import android.service.autofill.CustomDescription;
 import android.service.autofill.InternalOnClickAction;
@@ -196,7 +197,9 @@
                 }
                 intent.putExtra(AutofillManager.EXTRA_RESTORE_CROSS_ACTIVITY, true);
 
-                PendingIntent p = PendingIntent.getActivity(this, 0, intent, 0);
+                PendingIntent p = PendingIntent.getActivityAsUser(
+                        this, /* requestCode= */ 0, intent, /* flags= */ 0, /* options= */ null,
+                                UserHandle.CURRENT);
                 if (sDebug) {
                     Slog.d(TAG, "startActivity add save UI restored with intent=" + intent);
                 }
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index dc35c77..f6c4918 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -318,7 +318,7 @@
 
     private static final String SERIAL_ID_FILE = "serial_id";
 
-    private static final String SKIP_USER_FACING_DATA = "backup_skip_user_facing_data";
+    private static final String SKIP_USER_FACING_PACKAGES = "backup_skip_user_facing_packages";
     private static final String WALLPAPER_PACKAGE = "com.android.wallpaperbackup";
 
     private final @UserIdInt int mUserId;
@@ -3557,7 +3557,7 @@
     }
 
     /**
-     * We want to skip backup/restore of certain packages if 'backup_skip_user_facing_data' is
+     * We want to skip backup/restore of certain packages if 'backup_skip_user_facing_packages' is
      * set to true in secure settings. See b/153940088 for details.
      *
      * TODO(b/154822946): Remove this logic in the next release.
@@ -3581,7 +3581,7 @@
 
     @VisibleForTesting
     public boolean shouldSkipUserFacingData() {
-        return Settings.Secure.getInt(mContext.getContentResolver(), SKIP_USER_FACING_DATA,
+        return Settings.Secure.getInt(mContext.getContentResolver(), SKIP_USER_FACING_PACKAGES,
                 /* def */ 0) != 0;
     }
 
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b46bebb..6e8eca3 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -458,6 +458,12 @@
             "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?");
 
 
+    /** Automotive device unlockes users before system boot complete and this requires special
+     * handling as vold reset can lead into race conditions. When this is set, all users unlocked
+     * in {@code UserManager} level are unlocked after vold reset.
+     */
+    private final boolean mIsAutomotive;
+
     private VolumeInfo findVolumeByIdOrThrow(String id) {
         synchronized (mLock) {
             final VolumeInfo vol = mVolumes.get(id);
@@ -1082,14 +1088,12 @@
         Slog.d(TAG, "Thinking about reset, mBootCompleted=" + mBootCompleted
                 + ", mDaemonConnected=" + mDaemonConnected);
         if (mBootCompleted && mDaemonConnected) {
-            final List<UserInfo> users = mContext.getSystemService(UserManager.class).getUsers();
+            final UserManager userManager = mContext.getSystemService(UserManager.class);
+            final List<UserInfo> users = userManager.getUsers();
 
             if (mIsFuseEnabled) {
                 mStorageSessionController.onReset(mVold, () -> {
-                    mHandler.removeMessages(H_RESET);
-                    mHandler.removeMessages(H_VOLUME_BROADCAST);
-                    mHandler.removeMessages(H_VOLUME_MOUNT);
-                    mHandler.removeMessages(H_VOLUME_UNMOUNT);
+                    mHandler.removeCallbacksAndMessages(null);
                 });
             } else {
                 killMediaProvider(users);
@@ -1097,7 +1101,9 @@
 
             final int[] systemUnlockedUsers;
             synchronized (mLock) {
-                systemUnlockedUsers = mSystemUnlockedUsers;
+                // make copy as sorting can change order
+                systemUnlockedUsers = Arrays.copyOf(mSystemUnlockedUsers,
+                        mSystemUnlockedUsers.length);
 
                 mDisks.clear();
                 mVolumes.clear();
@@ -1119,6 +1125,9 @@
                     mVold.onUserStarted(userId);
                     mStoraged.onUserStarted(userId);
                 }
+                if (mIsAutomotive) {
+                    restoreAllUnlockedUsers(userManager, users, systemUnlockedUsers);
+                }
                 mVold.onSecureKeyguardStateChanged(mSecureKeyguardShowing);
                 mStorageManagerInternal.onReset(mVold);
             } catch (Exception e) {
@@ -1127,6 +1136,29 @@
         }
     }
 
+    private void restoreAllUnlockedUsers(UserManager userManager, List<UserInfo> allUsers,
+            int[] systemUnlockedUsers) throws Exception {
+        Arrays.sort(systemUnlockedUsers);
+        UserManager.invalidateIsUserUnlockedCache();
+        for (UserInfo user : allUsers) {
+            int userId = user.id;
+            if (!userManager.isUserRunning(userId)) {
+                continue;
+            }
+            if (Arrays.binarySearch(systemUnlockedUsers, userId) >= 0) {
+                continue;
+            }
+            boolean unlockingOrUnlocked = userManager.isUserUnlockingOrUnlocked(userId);
+            if (!unlockingOrUnlocked) {
+                continue;
+            }
+            Slog.w(TAG, "UNLOCK_USER lost from vold reset, will retry, user:" + userId);
+            mVold.onUserStarted(userId);
+            mStoraged.onUserStarted(userId);
+            mHandler.obtainMessage(H_COMPLETE_UNLOCK_USER, userId).sendToTarget();
+        }
+    }
+
     private void onUnlockUser(int userId) {
         Slog.d(TAG, "onUnlockUser " + userId);
 
@@ -1157,6 +1189,15 @@
         // Record user as started so newly mounted volumes kick off events
         // correctly, then synthesize events for any already-mounted volumes.
         synchronized (mLock) {
+            if (mIsAutomotive) {
+                for (int unlockedUser : mSystemUnlockedUsers) {
+                    if (unlockedUser == userId) {
+                        // This can happen as restoreAllUnlockedUsers can double post the message.
+                        Log.i(TAG, "completeUnlockUser called for already unlocked user:" + userId);
+                        return;
+                    }
+                }
+            }
             for (int i = 0; i < mVolumes.size(); i++) {
                 final VolumeInfo vol = mVolumes.valueAt(i);
                 if (vol.isVisibleForRead(userId) && vol.isMountedReadable()) {
@@ -1822,6 +1863,9 @@
         if (WATCHDOG_ENABLE) {
             Watchdog.getInstance().addMonitor(this);
         }
+
+        mIsAutomotive = context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_AUTOMOTIVE);
     }
 
     /**
@@ -4477,6 +4521,7 @@
             pw.println("Forced scoped storage app list: "
                     + DeviceConfig.getProperty(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                     PROP_FORCED_SCOPED_STORAGE_WHITELIST));
+            pw.println("isAutomotive:" + mIsAutomotive);
         }
 
         synchronized (mObbMounts) {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index f2c4e44..2d8d2c3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -348,6 +348,17 @@
     @GuardedBy("mUserIdToUserJourneyMap")
     private final SparseArray<UserJourneySession> mUserIdToUserJourneyMap = new SparseArray<>();
 
+    /**
+     * Sets on {@link #setInitialConfig(boolean, int, boolean)}, which is called by
+     * {@code ActivityManager} when the system is started.
+     *
+     * <p>It's useful to ignore external operations (i.e., originated outside {@code system_server},
+     * like from {@code adb shell am switch-user})) that could happen before such call is made and
+     * the system is ready.
+     */
+    @GuardedBy("mLock")
+    private boolean mInitialized;
+
     UserController(ActivityManagerService service) {
         this(new Injector(service));
     }
@@ -372,6 +383,7 @@
             mUserSwitchUiEnabled = userSwitchUiEnabled;
             mMaxRunningUsers = maxRunningUsers;
             mDelayUserDataLocking = delayUserDataLocking;
+            mInitialized = true;
         }
     }
 
@@ -1587,6 +1599,11 @@
         }
         boolean userSwitchUiEnabled;
         synchronized (mLock) {
+            if (!mInitialized) {
+                Slog.e(TAG, "Cannot switch to User #" + targetUserId
+                        + ": UserController not ready yet");
+                return false;
+            }
             mTargetUserId = targetUserId;
             userSwitchUiEnabled = mUserSwitchUiEnabled;
         }
@@ -2422,6 +2439,7 @@
             pw.println("  mDelayUserDataLocking:" + mDelayUserDataLocking);
             pw.println("  mMaxRunningUsers:" + mMaxRunningUsers);
             pw.println("  mUserSwitchUiEnabled:" + mUserSwitchUiEnabled);
+            pw.println("  mInitialized:" + mInitialized);
         }
     }
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 7e172a2..b5c173c 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -3744,7 +3744,7 @@
                     mHandler.sendMessage(PooledLambda.obtainMessage(
                             AppOpsService::notifyOpChangedForAllPkgsInUid,
                             this, code, uidState.uid, true, null));
-                } else {
+                } else if (uidState.pkgOps != null) {
                     final ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
                     if (callbacks != null) {
                         for (int cbi = callbacks.size() - 1; cbi >= 0; cbi--) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3627227..bbc24ea 100755
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -920,6 +920,8 @@
                 if (mHdmiManager != null) {
                     mHdmiManager.addHdmiControlStatusChangeListener(
                             mHdmiControlStatusChangeListenerCallback);
+                    mHdmiManager.addHdmiCecVolumeControlFeatureListener(mContext.getMainExecutor(),
+                            mMyHdmiCecVolumeControlFeatureListener);
                 }
                 mHdmiTvClient = mHdmiManager.getTvClient();
                 if (mHdmiTvClient != null) {
@@ -2248,6 +2250,7 @@
                 if (mHdmiManager != null) {
                     // mHdmiCecSink true => mHdmiPlaybackClient != null
                     if (mHdmiCecSink
+                            && mHdmiCecVolumeControlEnabled
                             && streamTypeAlias == AudioSystem.STREAM_MUSIC
                             // vol change on a full volume device
                             && isFullVolumeDevice(device)) {
@@ -2320,7 +2323,8 @@
     @GuardedBy("mHdmiClientLock")
     private void maybeSendSystemAudioStatusCommand(boolean isMuteAdjust) {
         if (mHdmiAudioSystemClient == null
-                || !mHdmiSystemAudioSupported) {
+                || !mHdmiSystemAudioSupported
+                || !mHdmiCecVolumeControlEnabled) {
             return;
         }
 
@@ -2340,7 +2344,8 @@
                     || mHdmiTvClient == null
                     || oldVolume == newVolume
                     || (flags & AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME) != 0
-                    || !mHdmiSystemAudioSupported) {
+                    || !mHdmiSystemAudioSupported
+                    || !mHdmiCecVolumeControlEnabled) {
                 return;
             }
             final long token = Binder.clearCallingIdentity();
@@ -2940,16 +2945,18 @@
         mVolumeController.postVolumeChanged(streamType, flags);
     }
 
-    // If Hdmi-CEC system audio mode is on and we are a TV panel, never show volume bar.
+    // Don't show volume UI when:
+    //  - Hdmi-CEC system audio mode is on and we are a TV panel
+    //  - CEC volume control enabled on a set-top box
     private int updateFlagsForTvPlatform(int flags) {
         synchronized (mHdmiClientLock) {
-            if (mHdmiTvClient != null && mHdmiSystemAudioSupported) {
+            if ((mHdmiTvClient != null && mHdmiSystemAudioSupported && mHdmiCecVolumeControlEnabled)
+                    || (mHdmiPlaybackClient != null && mHdmiCecVolumeControlEnabled)) {
                 flags &= ~AudioManager.FLAG_SHOW_UI;
             }
         }
         return flags;
     }
-
     // UI update and Broadcast Intent
     private void sendMasterMuteUpdate(boolean muted, int flags) {
         mVolumeController.postMasterMuteChanged(updateFlagsForTvPlatform(flags));
@@ -7141,9 +7148,21 @@
         }
     };
 
+    private class MyHdmiCecVolumeControlFeatureListener
+            implements HdmiControlManager.HdmiCecVolumeControlFeatureListener {
+        public void onHdmiCecVolumeControlFeature(boolean enabled) {
+            synchronized (mHdmiClientLock) {
+                if (mHdmiManager == null) return;
+                mHdmiCecVolumeControlEnabled = enabled;
+            }
+        }
+    };
+
     private final Object mHdmiClientLock = new Object();
 
     // If HDMI-CEC system audio is supported
+    // Note that for CEC volume commands mHdmiCecVolumeControlEnabled will play a role on volume
+    // commands
     private boolean mHdmiSystemAudioSupported = false;
     // Set only when device is tv.
     @GuardedBy("mHdmiClientLock")
@@ -7161,10 +7180,16 @@
     // Set only when device is an audio system.
     @GuardedBy("mHdmiClientLock")
     private HdmiAudioSystemClient mHdmiAudioSystemClient;
+    // True when volume control over HDMI CEC is used when CEC is enabled (meaningless otherwise)
+    @GuardedBy("mHdmiClientLock")
+    private boolean mHdmiCecVolumeControlEnabled;
 
     private MyHdmiControlStatusChangeListenerCallback mHdmiControlStatusChangeListenerCallback =
             new MyHdmiControlStatusChangeListenerCallback();
 
+    private MyHdmiCecVolumeControlFeatureListener mMyHdmiCecVolumeControlFeatureListener =
+            new MyHdmiCecVolumeControlFeatureListener();
+
     @Override
     public int setHdmiSystemAudioSupported(boolean on) {
         int device = AudioSystem.DEVICE_NONE;
@@ -7403,6 +7428,7 @@
         pw.print("  mHdmiPlaybackClient="); pw.println(mHdmiPlaybackClient);
         pw.print("  mHdmiTvClient="); pw.println(mHdmiTvClient);
         pw.print("  mHdmiSystemAudioSupported="); pw.println(mHdmiSystemAudioSupported);
+        pw.print("  mHdmiCecVolumeControlEnabled="); pw.println(mHdmiCecVolumeControlEnabled);
         pw.print("  mIsCallScreeningModeSupported="); pw.println(mIsCallScreeningModeSupported);
         pw.print("  mic mute FromSwitch=" + mMicMuteFromSwitch
                         + " FromRestrictions=" + mMicMuteFromRestrictions
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 0ac4f9e..f4a8667 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -213,6 +213,7 @@
         mLocalDeviceAddresses = initLocalDeviceAddresses();
         resetSelectRequestBuffer();
         launchDeviceDiscovery();
+        startQueuedActions();
     }
 
 
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 802a355..09fd33d 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -212,7 +212,7 @@
     private static final int DIMENSION_KEY_SIZE_HARD_LIMIT = 800;
     private static final int DIMENSION_KEY_SIZE_SOFT_LIMIT = 500;
     private static final long APP_OPS_SAMPLING_INITIALIZATION_DELAY_MILLIS = 45000;
-    private static final int APP_OPS_SIZE_ESTIMATE = 5000;
+    private static final int APP_OPS_SIZE_ESTIMATE = 2000;
 
     private static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity";
     /**
@@ -320,8 +320,7 @@
 
     private StatsPullAtomCallbackImpl mStatsCallbackImpl;
 
-    private final Object mAppOpsSamplingRateLock = new Object();
-    @GuardedBy("mAppOpsSamplingRateLock")
+    @GuardedBy("mAttributedAppOpsLock")
     private int mAppOpsSamplingRate = 0;
     private final Object mDangerousAppOpsListLock = new Object();
     @GuardedBy("mDangerousAppOpsListLock")
@@ -3084,7 +3083,7 @@
     int pullDangerousPermissionStateLocked(int atomTag, List<StatsEvent> pulledData) {
         final long token = Binder.clearCallingIdentity();
         float samplingRate = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_PERMISSIONS,
-                DANGEROUS_PERMISSION_STATE_SAMPLE_RATE, 0.02f);
+                DANGEROUS_PERMISSION_STATE_SAMPLE_RATE, 0.015f);
         Set<Integer> reportedUids = new HashSet<>();
         try {
             PackageManager pm = mContext.getPackageManager();
@@ -3479,23 +3478,21 @@
             HistoricalOps histOps = ops.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS,
                     TimeUnit.MILLISECONDS);
 
-            synchronized (mAppOpsSamplingRateLock) {
-                if (mAppOpsSamplingRate == 0) {
-                    mContext.getMainThreadHandler().postDelayed(new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                estimateAppOpsSamplingRate();
-                            } catch (Throwable e) {
-                                Slog.e(TAG, "AppOps sampling ratio estimation failed: ", e);
-                                synchronized (mAppOpsSamplingRateLock) {
-                                    mAppOpsSamplingRate = min(mAppOpsSamplingRate, 10);
-                                }
+            if (mAppOpsSamplingRate == 0) {
+                mContext.getMainThreadHandler().postDelayed(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            estimateAppOpsSamplingRate();
+                        } catch (Throwable e) {
+                            Slog.e(TAG, "AppOps sampling ratio estimation failed: ", e);
+                            synchronized (mAttributedAppOpsLock) {
+                                mAppOpsSamplingRate = min(mAppOpsSamplingRate, 10);
                             }
                         }
-                    }, APP_OPS_SAMPLING_INITIALIZATION_DELAY_MILLIS);
-                    mAppOpsSamplingRate = 100;
-                }
+                    }
+                }, APP_OPS_SAMPLING_INITIALIZATION_DELAY_MILLIS);
+                mAppOpsSamplingRate = 100;
             }
 
             List<AppOpEntry> opsList =
@@ -3503,9 +3500,7 @@
 
             int newSamplingRate = sampleAppOps(pulledData, opsList, atomTag, mAppOpsSamplingRate);
 
-            synchronized (mAppOpsSamplingRateLock) {
-                mAppOpsSamplingRate = min(mAppOpsSamplingRate, newSamplingRate);
-            }
+            mAppOpsSamplingRate = min(mAppOpsSamplingRate, newSamplingRate);
         } catch (Throwable t) {
             // TODO: catch exceptions at a more granular level
             Slog.e(TAG, "Could not read appops", t);
@@ -3544,7 +3539,7 @@
         }
         int estimatedSamplingRate = (int) constrain(
                 appOpsTargetCollectionSize * 100 / estimatedSize, 0, 100);
-        synchronized (mAppOpsSamplingRateLock) {
+        synchronized (mAttributedAppOpsLock) {
             mAppOpsSamplingRate = min(mAppOpsSamplingRate, estimatedSamplingRate);
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
new file mode 100644
index 0000000..b88573a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.hdmi;
+
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
+import static com.android.server.hdmi.Constants.ADDR_TV;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.IThermalService;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.test.TestLooper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(JUnit4.class)
+/** Tests for {@link HdmiCecLocalDeviceTv} class. */
+public class HdmiCecLocalDeviceTvTest {
+
+    private HdmiControlService mHdmiControlService;
+    private HdmiCecController mHdmiCecController;
+    private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv;
+    private FakeNativeWrapper mNativeWrapper;
+    private Looper mMyLooper;
+    private TestLooper mTestLooper = new TestLooper();
+    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
+    private int mTvPhysicalAddress;
+
+    @Mock
+    private IPowerManager mIPowerManagerMock;
+    @Mock
+    private IThermalService mIThermalServiceMock;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Context context = InstrumentationRegistry.getTargetContext();
+        mMyLooper = mTestLooper.getLooper();
+        PowerManager powerManager = new PowerManager(context, mIPowerManagerMock,
+                mIThermalServiceMock, new Handler(mMyLooper));
+        mHdmiControlService =
+                new HdmiControlService(InstrumentationRegistry.getTargetContext()) {
+                    @Override
+                    boolean isControlEnabled() {
+                        return true;
+                    }
+
+                    @Override
+                    boolean isTvDevice() {
+                        return true;
+                    }
+
+                    @Override
+                    void writeStringSystemProperty(String key, String value) {
+                        // do nothing
+                    }
+
+                    @Override
+                    PowerManager getPowerManager() {
+                        return powerManager;
+                    }
+                };
+
+        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
+        mHdmiCecLocalDeviceTv.init();
+        mHdmiControlService.setIoLooper(mMyLooper);
+        mNativeWrapper = new FakeNativeWrapper();
+        mHdmiCecController =
+                HdmiCecController.createWithNativeWrapper(mHdmiControlService, mNativeWrapper);
+        mHdmiControlService.setCecController(mHdmiCecController);
+        mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
+        mHdmiControlService.setMessageValidator(new HdmiCecMessageValidator(mHdmiControlService));
+        mLocalDevices.add(mHdmiCecLocalDeviceTv);
+        mHdmiControlService.initPortInfo();
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTvPhysicalAddress = 0x0000;
+        mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
+        mTestLooper.dispatchAll();
+        mNativeWrapper.clearResultMessages();
+    }
+
+    @Test
+    public void initialPowerStateIsStandby() {
+        assertThat(mHdmiCecLocalDeviceTv.getPowerStatus()).isEqualTo(
+                HdmiControlManager.POWER_STATUS_STANDBY);
+    }
+
+    @Test
+    public void onAddressAllocated_invokesDeviceDiscovery() {
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+        mHdmiCecLocalDeviceTv.onAddressAllocated(0, HdmiControlService.INITIATED_BY_BOOT_UP);
+
+        mTestLooper.dispatchAll();
+
+        // Check for for <Give Physical Address> being sent to available device (ADDR_PLAYBACK_1).
+        // This message is sent as part of the DeviceDiscoveryAction to available devices.
+        HdmiCecMessage givePhysicalAddress = HdmiCecMessageBuilder.buildGivePhysicalAddress(ADDR_TV,
+                ADDR_PLAYBACK_1);
+        assertThat(mNativeWrapper.getResultMessages()).contains(givePhysicalAddress);
+    }
+}
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index 1a38a42..bc987a6 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -303,12 +303,6 @@
             String message, boolean allowCarrierPrivilegeOnAnySub) {
         int uid = Binder.getCallingUid();
         int pid = Binder.getCallingPid();
-        PermissionManager permissionManager = (PermissionManager) context.getSystemService(
-                Context.PERMISSION_SERVICE);
-        if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, callingFeatureId,
-                pid, uid) == PackageManager.PERMISSION_GRANTED) {
-            return true;
-        }
 
         // If the calling package has carrier privileges for specified sub, then allow access.
         if (checkCarrierPrivilegeForSubId(context, subId)) return true;
@@ -319,6 +313,13 @@
             return true;
         }
 
+        PermissionManager permissionManager = (PermissionManager) context.getSystemService(
+                Context.PERMISSION_SERVICE);
+        if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, callingFeatureId,
+                pid, uid) == PackageManager.PERMISSION_GRANTED) {
+            return true;
+        }
+
         return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
                 message);
     }
@@ -433,16 +434,6 @@
     public static boolean checkReadPhoneNumber(
             Context context, int subId, int pid, int uid,
             String callingPackage, @Nullable String callingFeatureId, String message) {
-        // Default SMS app can always read it.
-        AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
-        if (appOps.noteOp(AppOpsManager.OPSTR_WRITE_SMS, uid, callingPackage, callingFeatureId,
-                null) == AppOpsManager.MODE_ALLOWED) {
-            return true;
-        }
-
-        // NOTE(b/73308711): If an app has one of the following AppOps bits explicitly revoked, they
-        // will be denied access, even if they have another permission and AppOps bit if needed.
-
         // First, check if the SDK version is below R
         boolean preR = false;
         try {
@@ -477,21 +468,29 @@
             }
         }
 
+        // Default SMS app can always read it.
+        AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+        if (appOps.noteOp(AppOpsManager.OPSTR_WRITE_SMS, uid, callingPackage, callingFeatureId,
+                null) == AppOpsManager.MODE_ALLOWED) {
+            return true;
+        }
         // Can be read with READ_SMS too.
         try {
             context.enforcePermission(android.Manifest.permission.READ_SMS, pid, uid, message);
-            return appOps.noteOp(AppOpsManager.OPSTR_READ_SMS, uid, callingPackage,
-                    callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
-
+            if (appOps.noteOp(AppOpsManager.OPSTR_READ_SMS, uid, callingPackage,
+                    callingFeatureId, null) == AppOpsManager.MODE_ALLOWED) {
+                return true;
+            }
         } catch (SecurityException readSmsSecurityException) {
         }
         // Can be read with READ_PHONE_NUMBERS too.
         try {
             context.enforcePermission(android.Manifest.permission.READ_PHONE_NUMBERS, pid, uid,
                     message);
-            return appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_NUMBERS, uid, callingPackage,
-                    callingFeatureId, null) == AppOpsManager.MODE_ALLOWED;
-
+            if (appOps.noteOp(AppOpsManager.OPSTR_READ_PHONE_NUMBERS, uid, callingPackage,
+                    callingFeatureId, null) == AppOpsManager.MODE_ALLOWED) {
+                return true;
+            }
         } catch (SecurityException readPhoneNumberSecurityException) {
         }
 
diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java
index d62cd0a..11667c8 100644
--- a/telephony/java/android/telephony/SubscriptionInfo.java
+++ b/telephony/java/android/telephony/SubscriptionInfo.java
@@ -305,11 +305,14 @@
     }
 
     /**
-     * Returns the ICC ID if the calling app has been granted the READ_PRIVILEGED_PHONE_STATE
-     * permission, has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}), or
-     * is a device owner or profile owner that has been granted the READ_PHONE_STATE permission.
-     * The profile owner is an app that owns a managed profile on the device; for more details see
-     * <a href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile
+     * Returns the ICC ID.
+     *
+     * Starting with API level 30, returns the ICC ID if the calling app has been granted the
+     * READ_PRIVILEGED_PHONE_STATE permission, has carrier privileges (see
+     * {@link TelephonyManager#hasCarrierPrivileges}), or is a device owner or profile owner that
+     * has been granted the READ_PHONE_STATE permission. The profile owner is an app that owns a
+     * managed profile on the device; for more details see <a
+     * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile
      * owner access is deprecated and will be removed in a future release.
      *
      * @return the ICC ID, or an empty string if one of these requirements is not met
@@ -449,8 +452,22 @@
     }
 
     /**
-     * @return the number of this subscription if the calling app has been granted the
-     * READ_PHONE_NUMBERS permission, or an empty string otherwise
+     * Returns the number of this subscription.
+     *
+     * Starting with API level 30, returns the number of this subscription if the calling app meets
+     * one of the following requirements:
+     * <ul>
+     *     <li>If the calling app's target SDK is API level 29 or lower and the app has been granted
+     *     the READ_PHONE_STATE permission.
+     *     <li>If the calling app has been granted any of READ_PRIVILEGED_PHONE_STATE,
+     *     READ_PHONE_NUMBERS, or READ_SMS.
+     *     <li>If the calling app has carrier privileges (see {@link
+     *     TelephonyManager#hasCarrierPrivileges}).
+     *     <li>If the calling app is the default SMS role holder.
+     * </ul>
+     *
+     * @return the number of this subscription, or an empty string if one of these requirements is
+     * not met
      */
     public String getNumber() {
         return mNumber;
@@ -670,12 +687,15 @@
     }
 
     /**
-     * Returns the card string if the calling app has been granted the READ_PRIVILEGED_PHONE_STATE
-     * permission, has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}), or
-     * is a device owner or profile owner on an organization owned device that has been granted the
-     * READ_PHONE_STATE permission. The profile owner is an app that owns a managed profile on the
-     * device; for more details see <a href="https://developer.android.com/work/managed-profiles">
-     * Work profiles</a>.
+     * Returns the card string of the SIM card which contains the subscription.
+     *
+     * Starting with API level 30, returns the card string if the calling app has been granted the
+     * READ_PRIVILEGED_PHONE_STATE permission, has carrier privileges (see
+     * {@link TelephonyManager#hasCarrierPrivileges}), or is a device owner or profile owner that
+     * has been granted the READ_PHONE_STATE permission. The profile owner is an app that owns a
+     * managed profile on the device; for more details see <a
+     * href="https://developer.android.com/work/managed-profiles">Work profiles</a>. Profile
+     * owner access is deprecated and will be removed in a future release.
      *
      * @return the card string of the SIM card which contains the subscription or an empty string
      * if these requirements are not met. The card string is the ICCID for UICCs or the EID for
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
index 4a0ca66..2df0024 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/DummyBlobData.java
@@ -153,7 +153,14 @@
     public void writeToSession(BlobStoreManager.Session session,
             long offsetBytes, long lengthBytes) throws Exception {
         try (FileInputStream in = new FileInputStream(mFile)) {
-            Utils.writeToSession(session, in, offsetBytes, lengthBytes);
+            Utils.writeToSession(session, in, offsetBytes, lengthBytes, lengthBytes);
+        }
+    }
+
+    public void writeToSession(BlobStoreManager.Session session,
+            long offsetBytes, long lengthBytes, long allocateBytes) throws Exception {
+        try (FileInputStream in = new FileInputStream(mFile)) {
+            Utils.writeToSession(session, in, offsetBytes, lengthBytes, allocateBytes);
         }
     }
 
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
index b9bd661..ec85995 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
@@ -59,15 +59,15 @@
     public static void writeToSession(BlobStoreManager.Session session, ParcelFileDescriptor input,
             long lengthBytes) throws IOException {
         try (FileInputStream in = new ParcelFileDescriptor.AutoCloseInputStream(input)) {
-            writeToSession(session, in, 0, lengthBytes);
+            writeToSession(session, in, 0, lengthBytes, lengthBytes);
         }
     }
 
     public static void writeToSession(BlobStoreManager.Session session, FileInputStream in,
-            long offsetBytes, long lengthBytes) throws IOException {
+            long offsetBytes, long lengthBytes, long allocateBytes) throws IOException {
         in.getChannel().position(offsetBytes);
         try (FileOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(
-                session.openWrite(offsetBytes, lengthBytes))) {
+                session.openWrite(offsetBytes, allocateBytes))) {
             copy(in, out, lengthBytes);
         }
     }
diff --git a/tools/stats_log_api_gen/Android.bp b/tools/stats_log_api_gen/Android.bp
index b1e2487..e3b6db0 100644
--- a/tools/stats_log_api_gen/Android.bp
+++ b/tools/stats_log_api_gen/Android.bp
@@ -21,7 +21,6 @@
     name: "stats-log-api-gen",
     srcs: [
         "Collation.cpp",
-        "atoms_info_writer.cpp",
         "java_writer.cpp",
         "java_writer_q.cpp",
         "main.cpp",
diff --git a/tools/stats_log_api_gen/Collation.cpp b/tools/stats_log_api_gen/Collation.cpp
index 958e94e..a230de4 100644
--- a/tools/stats_log_api_gen/Collation.cpp
+++ b/tools/stats_log_api_gen/Collation.cpp
@@ -52,9 +52,7 @@
       defaultState(that.defaultState),
       triggerStateReset(that.triggerStateReset),
       nested(that.nested),
-      uidField(that.uidField),
-      whitelisted(that.whitelisted),
-      truncateTimestamp(that.truncateTimestamp) {
+      uidField(that.uidField) {
 }
 
 AtomDecl::AtomDecl(int c, const string& n, const string& m) : code(c), name(n), message(m) {
@@ -520,13 +518,6 @@
         shared_ptr<AtomDecl> atomDecl =
                 make_shared<AtomDecl>(atomField->number(), atomField->name(), atom->name());
 
-        if (atomField->options().GetExtension(os::statsd::allow_from_any_uid) == true) {
-            atomDecl->whitelisted = true;
-            if (dbg) {
-                printf("%s is whitelisted\n", atomField->name().c_str());
-            }
-        }
-
         if (atomDecl->code < PULL_ATOM_START_ID &&
             atomField->options().GetExtension(os::statsd::truncate_timestamp)) {
             addAnnotationToAtomDecl(atomDecl.get(), ATOM_ID_FIELD_NUMBER,
diff --git a/tools/stats_log_api_gen/Collation.h b/tools/stats_log_api_gen/Collation.h
index 043f8b1..10b34ec 100644
--- a/tools/stats_log_api_gen/Collation.h
+++ b/tools/stats_log_api_gen/Collation.h
@@ -164,10 +164,6 @@
 
     int uidField = 0;
 
-    bool whitelisted = false;
-
-    bool truncateTimestamp = false;
-
     AtomDecl();
     AtomDecl(const AtomDecl& that);
     AtomDecl(int code, const string& name, const string& message);
diff --git a/tools/stats_log_api_gen/atoms_info_writer.cpp b/tools/stats_log_api_gen/atoms_info_writer.cpp
deleted file mode 100644
index 292cb21..0000000
--- a/tools/stats_log_api_gen/atoms_info_writer.cpp
+++ /dev/null
@@ -1,91 +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.
- */
-
-#include "atoms_info_writer.h"
-
-#include <map>
-#include <set>
-#include <vector>
-
-#include "utils.h"
-
-namespace android {
-namespace stats_log_api_gen {
-
-static void write_atoms_info_header_body(FILE* out) {
-    fprintf(out, "struct AtomsInfo {\n");
-    fprintf(out, "  const static std::set<int> kWhitelistedAtoms;\n");
-    fprintf(out, "};\n");
-}
-
-static void write_atoms_info_cpp_body(FILE* out, const Atoms& atoms) {
-
-    fprintf(out, "const std::set<int> AtomsInfo::kWhitelistedAtoms = {\n");
-    for (AtomDeclSet::const_iterator atomIt = atoms.decls.begin(); atomIt != atoms.decls.end();
-         atomIt++) {
-        if ((*atomIt)->whitelisted) {
-            const string constant = make_constant_name((*atomIt)->name);
-            fprintf(out, "    %d, // %s\n", (*atomIt)->code, constant.c_str());
-        }
-    }
-
-    fprintf(out, "};\n");
-    fprintf(out, "\n");
-
-}
-
-int write_atoms_info_header(FILE* out, const string& namespaceStr) {
-    // Print prelude
-    fprintf(out, "// This file is autogenerated\n");
-    fprintf(out, "\n");
-    fprintf(out, "#pragma once\n");
-    fprintf(out, "\n");
-    fprintf(out, "#include <vector>\n");
-    fprintf(out, "#include <map>\n");
-    fprintf(out, "#include <set>\n");
-    fprintf(out, "\n");
-
-    write_namespace(out, namespaceStr);
-
-    write_atoms_info_header_body(out);
-
-    fprintf(out, "\n");
-    write_closing_namespace(out, namespaceStr);
-
-    return 0;
-}
-
-int write_atoms_info_cpp(FILE* out, const Atoms& atoms, const string& namespaceStr,
-                         const string& importHeader) {
-    // Print prelude
-    fprintf(out, "// This file is autogenerated\n");
-    fprintf(out, "\n");
-    fprintf(out, "#include <%s>\n", importHeader.c_str());
-    fprintf(out, "\n");
-
-    write_namespace(out, namespaceStr);
-
-    write_atoms_info_cpp_body(out, atoms);
-
-    // Print footer
-    fprintf(out, "\n");
-    write_closing_namespace(out, namespaceStr);
-
-    return 0;
-}
-
-}  // namespace stats_log_api_gen
-}  // namespace android
diff --git a/tools/stats_log_api_gen/atoms_info_writer.h b/tools/stats_log_api_gen/atoms_info_writer.h
deleted file mode 100644
index 09a4303..0000000
--- a/tools/stats_log_api_gen/atoms_info_writer.h
+++ /dev/null
@@ -1,35 +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.
- */
-
-#pragma once
-
-#include <stdio.h>
-#include <string.h>
-
-#include "Collation.h"
-
-namespace android {
-namespace stats_log_api_gen {
-
-using namespace std;
-
-int write_atoms_info_cpp(FILE* out, const Atoms& atoms, const string& namespaceStr,
-                         const string& importHeader);
-
-int write_atoms_info_header(FILE* out, const string& namespaceStr);
-
-}  // namespace stats_log_api_gen
-}  // namespace android
diff --git a/tools/stats_log_api_gen/main.cpp b/tools/stats_log_api_gen/main.cpp
index 136933b..b888ce9 100644
--- a/tools/stats_log_api_gen/main.cpp
+++ b/tools/stats_log_api_gen/main.cpp
@@ -9,7 +9,6 @@
 #include <vector>
 
 #include "Collation.h"
-#include "atoms_info_writer.h"
 #include "frameworks/base/cmds/statsd/src/atoms.pb.h"
 #include "java_writer.h"
 #include "java_writer_q.h"
@@ -30,12 +29,6 @@
     fprintf(stderr, "OPTIONS\n");
     fprintf(stderr, "  --cpp FILENAME       the header file to output for write helpers\n");
     fprintf(stderr, "  --header FILENAME    the cpp file to output for write helpers\n");
-    fprintf(stderr,
-            "  --atomsInfoCpp FILENAME       the header file to output for "
-            "statsd metadata\n");
-    fprintf(stderr,
-            "  --atomsInfoHeader FILENAME    the cpp file to output for statsd "
-            "metadata\n");
     fprintf(stderr, "  --help               this message\n");
     fprintf(stderr, "  --java FILENAME      the java file to output\n");
     fprintf(stderr, "  --module NAME        optional, module name to generate outputs for\n");
@@ -49,10 +42,6 @@
             "  --importHeader NAME  required for cpp/jni to say which header to "
             "import "
             "for write helpers\n");
-    fprintf(stderr,
-            "  --atomsInfoImportHeader NAME  required for cpp to say which "
-            "header to import "
-            "for statsd metadata\n");
     fprintf(stderr, "  --javaPackage PACKAGE             the package for the java file.\n");
     fprintf(stderr, "                                    required for java with module\n");
     fprintf(stderr, "  --javaClass CLASS    the class name of the java class.\n");
@@ -74,15 +63,12 @@
     string cppFilename;
     string headerFilename;
     string javaFilename;
-    string atomsInfoCppFilename;
-    string atomsInfoHeaderFilename;
     string javaPackage;
     string javaClass;
 
     string moduleName = DEFAULT_MODULE_NAME;
     string cppNamespace = DEFAULT_CPP_NAMESPACE;
     string cppHeaderImport = DEFAULT_CPP_HEADER_IMPORT;
-    string atomsInfoCppHeaderImport = DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT;
     bool supportQ = false;
     bool supportWorkSource = false;
     bool compileQ = false;
@@ -148,27 +134,6 @@
                 return 1;
             }
             javaClass = argv[index];
-        } else if (0 == strcmp("--atomsInfoHeader", argv[index])) {
-            index++;
-            if (index >= argc) {
-                print_usage();
-                return 1;
-            }
-            atomsInfoHeaderFilename = argv[index];
-        } else if (0 == strcmp("--atomsInfoCpp", argv[index])) {
-            index++;
-            if (index >= argc) {
-                print_usage();
-                return 1;
-            }
-            atomsInfoCppFilename = argv[index];
-        } else if (0 == strcmp("--atomsInfoImportHeader", argv[index])) {
-            index++;
-            if (index >= argc) {
-                print_usage();
-                return 1;
-            }
-            atomsInfoCppHeaderImport = argv[index];
         } else if (0 == strcmp("--supportQ", argv[index])) {
             supportQ = true;
         } else if (0 == strcmp("--worksource", argv[index])) {
@@ -180,8 +145,7 @@
         index++;
     }
 
-    if (cppFilename.size() == 0 && headerFilename.size() == 0 && javaFilename.size() == 0 &&
-        atomsInfoHeaderFilename.size() == 0 && atomsInfoCppFilename.size() == 0) {
+    if (cppFilename.size() == 0 && headerFilename.size() == 0 && javaFilename.size() == 0) {
         print_usage();
         return 1;
     }
@@ -210,29 +174,6 @@
     collate_atom(android::os::statsd::AttributionNode::descriptor(), &attributionDecl,
                  &attributionSignature);
 
-    // Write the atoms info .cpp file
-    if (atomsInfoCppFilename.size() != 0) {
-        FILE* out = fopen(atomsInfoCppFilename.c_str(), "w");
-        if (out == NULL) {
-            fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoCppFilename.c_str());
-            return 1;
-        }
-        errorCount = android::stats_log_api_gen::write_atoms_info_cpp(out, atoms, cppNamespace,
-                                                                      atomsInfoCppHeaderImport);
-        fclose(out);
-    }
-
-    // Write the atoms info .h file
-    if (atomsInfoHeaderFilename.size() != 0) {
-        FILE* out = fopen(atomsInfoHeaderFilename.c_str(), "w");
-        if (out == NULL) {
-            fprintf(stderr, "Unable to open file for write: %s\n", atomsInfoHeaderFilename.c_str());
-            return 1;
-        }
-        errorCount = android::stats_log_api_gen::write_atoms_info_header(out, cppNamespace);
-        fclose(out);
-    }
-
     // Write the .cpp file
     if (cppFilename.size() != 0) {
         FILE* out = fopen(cppFilename.c_str(), "w");
diff --git a/tools/stats_log_api_gen/test.proto b/tools/stats_log_api_gen/test.proto
index d22acc6..aaa488e 100644
--- a/tools/stats_log_api_gen/test.proto
+++ b/tools/stats_log_api_gen/test.proto
@@ -187,24 +187,6 @@
     optional int32 state = 3 [(android.os.statsd.state_field_option).exclusive_state = true];
 }
 
-message WhitelistedAtom {
-  optional int32 field = 1;
-}
-
-message NonWhitelistedAtom {
-  optional int32 field = 1;
-}
-
-message ListedAtoms {
-  oneof event {
-    // Atoms can be whitelisted i.e. they can be triggered by any source
-    WhitelistedAtom whitelisted_atom = 1 [(android.os.statsd.allow_from_any_uid) = true];
-    // Atoms are not whitelisted by default, so they can only be triggered
-    // by whitelisted sources
-    NonWhitelistedAtom non_whitelisted_atom = 2;
-  }
-}
-
 message ModuleOneAtom {
     optional int32 field = 1 [(android.os.statsd.is_uid) = true];
 }
diff --git a/tools/stats_log_api_gen/test_collation.cpp b/tools/stats_log_api_gen/test_collation.cpp
index 1504752..dbae588 100644
--- a/tools/stats_log_api_gen/test_collation.cpp
+++ b/tools/stats_log_api_gen/test_collation.cpp
@@ -225,25 +225,6 @@
     EXPECT_TRUE(errorCount > 0);
 }
 
-TEST(CollationTest, PassOnWhitelistedAtom) {
-    Atoms atoms;
-    int errorCount = collate_atoms(ListedAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms);
-    EXPECT_EQ(errorCount, 0);
-    EXPECT_EQ(atoms.decls.size(), 2ul);
-}
-
-TEST(CollationTest, RecogniseWhitelistedAtom) {
-    Atoms atoms;
-    collate_atoms(ListedAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms);
-    for (const auto& atomDecl : atoms.decls) {
-        if (atomDecl->code == 1) {
-            EXPECT_TRUE(atomDecl->whitelisted);
-        } else {
-            EXPECT_FALSE(atomDecl->whitelisted);
-        }
-    }
-}
-
 TEST(CollationTest, PassOnLogFromModuleAtom) {
     Atoms atoms;
     int errorCount = collate_atoms(ModuleAtoms::descriptor(), DEFAULT_MODULE_NAME, &atoms);
diff --git a/tools/stats_log_api_gen/utils.h b/tools/stats_log_api_gen/utils.h
index 7d6d08e..73e0cb8 100644
--- a/tools/stats_log_api_gen/utils.h
+++ b/tools/stats_log_api_gen/utils.h
@@ -32,7 +32,6 @@
 
 const string DEFAULT_CPP_NAMESPACE = "android,util";
 const string DEFAULT_CPP_HEADER_IMPORT = "statslog.h";
-const string DEFAULT_ATOMS_INFO_CPP_HEADER_IMPORT = "atoms_info.h";
 
 const int JAVA_MODULE_REQUIRES_FLOAT = 0x01;
 const int JAVA_MODULE_REQUIRES_ATTRIBUTION = 0x02;
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 1b0497a..fb6af5b 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -928,19 +928,26 @@
 
     /**
      * Broadcast intent action indicating that the configured networks changed.
-     * This can be as a result of adding/updating/deleting a network. If
-     * {@link #EXTRA_MULTIPLE_NETWORKS_CHANGED} is set to true the new configuration
-     * can be retreived with the {@link #EXTRA_WIFI_CONFIGURATION} extra. If multiple
-     * Wi-Fi configurations changed, {@link #EXTRA_WIFI_CONFIGURATION} will not be present.
+     * This can be as a result of adding/updating/deleting a network.
+     * <br />
+     * {@link #EXTRA_CHANGE_REASON} contains whether the configuration was added/changed/removed.
+     * {@link #EXTRA_WIFI_CONFIGURATION} is never set starting in Android 11.
+     * {@link #EXTRA_MULTIPLE_NETWORKS_CHANGED} is set for backwards compatibility reasons, but
+     * its value is always true, even if only a single network changed.
+     * <br />
+     * The {@link android.Manifest.permission#ACCESS_WIFI_STATE ACCESS_WIFI_STATE} permission is
+     * required to receive this broadcast.
+     *
      * @hide
      */
     @SystemApi
     public static final String CONFIGURED_NETWORKS_CHANGED_ACTION =
         "android.net.wifi.CONFIGURED_NETWORKS_CHANGE";
     /**
-     * The lookup key for a (@link android.net.wifi.WifiConfiguration} object representing
+     * The lookup key for a {@link android.net.wifi.WifiConfiguration} object representing
      * the changed Wi-Fi configuration when the {@link #CONFIGURED_NETWORKS_CHANGED_ACTION}
      * broadcast is sent.
+     * Note: this extra is never set starting in Android 11.
      * @hide
      */
     @SystemApi
@@ -948,14 +955,16 @@
     /**
      * Multiple network configurations have changed.
      * @see #CONFIGURED_NETWORKS_CHANGED_ACTION
-     *
+     * Note: this extra is always true starting in Android 11.
      * @hide
      */
     @SystemApi
     public static final String EXTRA_MULTIPLE_NETWORKS_CHANGED = "multipleChanges";
     /**
      * The lookup key for an integer indicating the reason a Wi-Fi network configuration
-     * has changed. Only present if {@link #EXTRA_MULTIPLE_NETWORKS_CHANGED} is {@code false}
+     * has changed. One of {@link #CHANGE_REASON_ADDED}, {@link #CHANGE_REASON_REMOVED},
+     * {@link #CHANGE_REASON_CONFIG_CHANGE}.
+     *
      * @see #CONFIGURED_NETWORKS_CHANGED_ACTION
      * @hide
      */