Merge "Removing the padding for launch animation for dialog transitions." into main
diff --git a/api/Android.bp b/api/Android.bp
index cd1997c..6a04f0d 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -368,8 +368,6 @@
         "--hide CallbackInterface",
         // Disable HiddenSuperclass, as Metalava handles this fine (it should be hidden by default)
         "--hide HiddenSuperclass",
-        "--hide-package android.audio.policy.configuration.V7_0",
-        "--hide-package com.android.server",
         "--manifest $(location :frameworks-base-core-AndroidManifest.xml)",
     ],
     filter_packages: packages_to_document,
diff --git a/core/api/current.txt b/core/api/current.txt
index f2c59dac..9ad8dd3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6844,6 +6844,9 @@
     method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri);
   }
 
+  @FlaggedApi("android.app.api_rich_ongoing") public abstract static class Notification.RichOngoingStyle extends android.app.Notification.Style {
+  }
+
   public abstract static class Notification.Style {
     ctor @Deprecated public Notification.Style();
     method public android.app.Notification build();
@@ -12044,6 +12047,7 @@
     field public static final int COLOR_MODE_DEFAULT = 0; // 0x0
     field public static final int COLOR_MODE_HDR = 2; // 0x2
     field public static final int COLOR_MODE_WIDE_COLOR_GAMUT = 1; // 0x1
+    field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_ASSETS_PATHS = -2147483648; // 0x80000000
     field public static final int CONFIG_COLOR_MODE = 16384; // 0x4000
     field public static final int CONFIG_DENSITY = 4096; // 0x1000
     field public static final int CONFIG_FONT_SCALE = 1073741824; // 0x40000000
@@ -12057,6 +12061,7 @@
     field public static final int CONFIG_MNC = 2; // 0x2
     field public static final int CONFIG_NAVIGATION = 64; // 0x40
     field public static final int CONFIG_ORIENTATION = 128; // 0x80
+    field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_RESOURCES_UNUSED = 134217728; // 0x8000000
     field public static final int CONFIG_SCREEN_LAYOUT = 256; // 0x100
     field public static final int CONFIG_SCREEN_SIZE = 1024; // 0x400
     field public static final int CONFIG_SMALLEST_SCREEN_SIZE = 2048; // 0x800
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b384326..36b1eab 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -30,6 +30,7 @@
 import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
 import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS;
 import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
 import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
 import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
 import static android.view.Display.DEFAULT_DISPLAY;
@@ -41,7 +42,6 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;
 import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext;
-import static com.android.window.flags.Flags.activityWindowInfoFlag;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -6520,6 +6520,13 @@
             return true;
         }
 
+        if (android.content.res.Flags.handleAllConfigChanges()) {
+            if ((handledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0) {
+                // Report the change if activities claim they do not use resources at all.
+                return true;
+            }
+        }
+
         final int diffWithBucket = SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig,
                 newConfig, sizeBuckets);
         // Compare to the diff which filter the change without crossing size buckets with
@@ -6846,9 +6853,6 @@
     }
 
     private void handleActivityWindowInfoChanged(@NonNull ActivityClientRecord r) {
-        if (!activityWindowInfoFlag()) {
-            return;
-        }
         if (r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) {
             return;
         }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index db979a5..ef09dc4 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -10979,6 +10979,18 @@
     }
 
     /**
+     * An object that can apply a rich ongoing notification style to a {@link Notification.Builder}
+     * object.
+     */
+    @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+    public abstract static class RichOngoingStyle extends Notification.Style {
+        /**
+         * @hide
+         */
+        public RichOngoingStyle() {}
+    }
+
+    /**
      * Notification style for custom views that are decorated by the system
      *
      * <p>Instead of providing a notification that is completely custom, a developer can set this
diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
index 0c1e7a3..c281533 100644
--- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java
+++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
@@ -19,8 +19,6 @@
 import static android.app.WindowConfiguration.areConfigurationsEqualForDisplay;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static com.android.window.flags.Flags.activityWindowInfoFlag;
-
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
@@ -102,9 +100,6 @@
      */
     public void registerActivityWindowInfoChangedListener(
             @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
-        if (!activityWindowInfoFlag()) {
-            return;
-        }
         synchronized (mLock) {
             mActivityWindowInfoChangedListeners.add(listener);
         }
@@ -116,9 +111,6 @@
      */
     public void unregisterActivityWindowInfoChangedListener(
             @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
-        if (!activityWindowInfoFlag()) {
-            return;
-        }
         synchronized (mLock) {
             mActivityWindowInfoChangedListeners.remove(listener);
         }
@@ -130,9 +122,6 @@
      */
     public void onActivityWindowInfoChanged(@NonNull IBinder activityToken,
             @NonNull ActivityWindowInfo activityWindowInfo) {
-        if (!activityWindowInfoFlag()) {
-            return;
-        }
         final Object[] activityWindowInfoChangedListeners;
         synchronized (mLock) {
             if (mActivityWindowInfoChangedListeners.isEmpty()) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/core/java/android/audio/policy/configuration/V7_0/package-info.java
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to core/java/android/audio/policy/configuration/V7_0/package-info.java
index 0e4c923..8f7425f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/core/java/android/audio/policy/configuration/V7_0/package-info.java
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+/**
+ * Hide the android.audio.policy.configuration.V7_0 API as that is managed
+ * separately.
+ *
+ * @hide
+ */
+package android.audio.policy.configuration.V7_0;
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e0553c8..385d6cfd 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -61,6 +61,7 @@
 import android.permission.PermissionCheckerManager;
 import android.provider.MediaStore;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseBooleanArray;
 
@@ -486,6 +487,8 @@
             validateIncomingAuthority(authority);
             int numOperations = operations.size();
             final int[] userIds = new int[numOperations];
+            final ArraySet<String> readPermissions = new ArraySet<String>();
+            final ArraySet<String> writePermissions = new ArraySet<String>();
             for (int i = 0; i < numOperations; i++) {
                 ContentProviderOperation operation = operations.get(i);
                 Uri uri = operation.getUri();
@@ -499,17 +502,19 @@
                 }
                 final AttributionSource accessAttributionSource =
                         attributionSource;
-                if (operation.isReadOperation()) {
+                if (operation.isReadOperation() && !readPermissions.contains(uri.toString())) {
                     if (enforceReadPermission(accessAttributionSource, uri)
                             != PermissionChecker.PERMISSION_GRANTED) {
                         throw new OperationApplicationException("App op not allowed", 0);
                     }
+                    readPermissions.add(uri.toString());
                 }
-                if (operation.isWriteOperation()) {
+                if (operation.isWriteOperation() && !writePermissions.contains(uri.toString())) {
                     if (enforceWritePermission(accessAttributionSource, uri)
                             != PermissionChecker.PERMISSION_GRANTED) {
                         throw new OperationApplicationException("App op not allowed", 0);
                     }
+                    writePermissions.add(uri.toString());
                 }
             }
             traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "applyBatch: ", authority);
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 57ffed4..6952a09 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -941,6 +941,8 @@
             CONFIG_COLOR_MODE,
             CONFIG_FONT_SCALE,
             CONFIG_GRAMMATICAL_GENDER,
+            CONFIG_ASSETS_PATHS,
+            CONFIG_RESOURCES_UNUSED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Config {}
@@ -1060,8 +1062,8 @@
      * can itself handle asset path changes.  Set from the {@link android.R.attr#configChanges}
      * attribute. This is not a core resource configuration, but a higher-level value, so its
      * constant starts at the high bits.
-     * @hide We do not want apps handling this yet, but we do need some kind of bit for diffs.
      */
+    @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
     public static final int CONFIG_ASSETS_PATHS = 0x80000000;
     /**
      * Bit in {@link #configChanges} that indicates that the activity
@@ -1088,6 +1090,30 @@
      */
     public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 0x10000000;
 
+    /**
+     * <p>This is probably not the constant you want, the resources compiler supports a less
+     * dangerous version of it, 'allKnown', that only suppresses all currently existing
+     * configuration change restarts depending on your target SDK rather than whatever the latest
+     * SDK supports, allowing the application to work with resources on future Platform versions.
+     *
+     * <p>Bit in {@link #configChanges} that indicates that the activity doesn't use Android
+     * Resources at all and doesn't need to be restarted on any configuration changes. This bit
+     * disables all restarts for configuration dimensions available in the current target SDK as
+     * well as dimensions introduced in future SDKs. Use it only if the activity doesn't need
+     * anything from its resources, and doesn't depend on any libraries that may provide resources
+     * and need to respond to configuration changes. When set,
+     * {@link Activity#onConfigurationChanged(Configuration)} will be called instead of a restart,
+     * and it’s up to the implementation to ensure that no stale resource values remain loaded
+     * anywhere in the code.
+     *
+     * <p>This overrides all other bits, and this is recommended to be used individually.
+     *
+     * <p>This is not a core resource configuration, but a higher-level value, so its constant
+     * starts at the high bits.
+     */
+    @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+    public static final int CONFIG_RESOURCES_UNUSED = 0x8000000;
+
     /** @hide
      * Unfortunately the constants for config changes in native code are
      * different from ActivityInfo. :(  Here are the values we should use for the
@@ -1657,7 +1683,8 @@
      * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION},
      * {@link #CONFIG_ORIENTATION}, {@link #CONFIG_SCREEN_LAYOUT},
      * {@link #CONFIG_DENSITY}, {@link #CONFIG_LAYOUT_DIRECTION},
-     * {@link #CONFIG_COLOR_MODE}, and {link #CONFIG_GRAMMATICAL_GENDER}.
+     * {@link #CONFIG_COLOR_MODE}, {@link #CONFIG_GRAMMATICAL_GENDER},
+     * {@link #CONFIG_ASSETS_PATHS}, and {@link #CONFIG_RESOURCES_UNUSED}.
      * Set from the {@link android.R.attr#configChanges} attribute.
      */
     public int configChanges;
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index c7d94c6..26ee4e8 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -161,6 +161,16 @@
 }
 
 flag {
+    name: "cache_profile_parent"
+    namespace: "multiuser"
+    description: "Cache getProfileParent to avoid unnecessary binder calls"
+    bug: "350417399"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "cache_quiet_mode_state"
     namespace: "multiuser"
     description: "Optimise quiet mode state retrieval"
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index a475cc8..a5f8199 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -56,3 +56,13 @@
     # This flag is read in ResourcesImpl at boot time.
     is_fixed_read_only: true
 }
+
+flag {
+    name: "handle_all_config_changes"
+    is_exported: true
+    namespace: "resource_manager"
+    description: "Feature flag for allowing activities to handle all kinds of configuration changes"
+    bug: "180625460"
+    # This flag is read at boot time.
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 6201359..6a7ee7f 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -56,6 +56,8 @@
 import android.hardware.camera2.utils.ExceptionUtils;
 import android.hardware.devicestate.DeviceState;
 import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.feature.flags.FeatureFlags;
+import android.hardware.devicestate.feature.flags.FeatureFlagsImpl;
 import android.hardware.display.DisplayManager;
 import android.os.Binder;
 import android.os.Handler;
@@ -247,14 +249,22 @@
         private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners =
                 new ArrayList<>();
         private boolean mFoldedDeviceState;
+        private final FeatureFlags mDeviceStateManagerFlags;
 
         public FoldStateListener(Context context) {
             mFoldedDeviceStates = context.getResources().getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
+            mDeviceStateManagerFlags = new FeatureFlagsImpl();
         }
 
-        private synchronized void handleStateChange(int state) {
-            boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+        private synchronized void handleStateChange(DeviceState state) {
+            final boolean folded;
+            if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+                folded = state.hasProperty(
+                        DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY);
+            } else {
+                folded = ArrayUtils.contains(mFoldedDeviceStates, state.getIdentifier());
+            }
 
             mFoldedDeviceState = folded;
             Iterator<WeakReference<DeviceStateListener>> it = mDeviceStateListeners.iterator();
@@ -276,10 +286,8 @@
 
         @SuppressWarnings("FlaggedApi")
         @Override
-        public void onDeviceStateChanged(DeviceState state) {
-            // Suppressing the FlaggedAPI warning as this specific API isn't new, just moved to
-            // system API which requires it to be flagged.
-            handleStateChange(state.getIdentifier());
+        public void onDeviceStateChanged(@NonNull DeviceState state) {
+            handleStateChange(state);
         }
     }
 
diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java
index febc24c1..5b511cc 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManager.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManager.java
@@ -67,6 +67,14 @@
     public static final int MAXIMUM_DEVICE_STATE_IDENTIFIER = 10000;
 
     /**
+     * {@link DeviceState} to represent an invalid device state.
+     * @hide
+     */
+    public static final DeviceState INVALID_DEVICE_STATE = new DeviceState(
+            new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
+                    "INVALID").build());
+
+    /**
      * Intent needed to launch the rear display overlay activity from SysUI
      *
      * @hide
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 4894fb1..0321e1df 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -395,7 +395,7 @@
      * the display is removed.
      *
      * Public virtual displays without this flag will move their content to main display
-     * stack once they're removed. Private vistual displays will always destroy their
+     * stack once they're removed. Private virtual displays will always destroy their
      * content on removal even without this flag.
      *
      * @see #createVirtualDisplay
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 91caedc..811a834 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -686,8 +686,12 @@
         public RefreshRateRange range;
 
         public RefreshRateLimitation(@RefreshRateLimitType int type, float min, float max) {
+            this(type, new RefreshRateRange(min, max));
+        }
+
+        public RefreshRateLimitation(@RefreshRateLimitType int type, RefreshRateRange range) {
             this.type = type;
-            range = new RefreshRateRange(min, max);
+            this.range = range;
         }
 
         @Override
diff --git a/core/java/android/inputmethodservice/ImsConfigurationTracker.java b/core/java/android/inputmethodservice/ImsConfigurationTracker.java
index 30ef0a2..17a5ffc 100644
--- a/core/java/android/inputmethodservice/ImsConfigurationTracker.java
+++ b/core/java/android/inputmethodservice/ImsConfigurationTracker.java
@@ -16,6 +16,8 @@
 
 package android.inputmethodservice;
 
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
+
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -88,12 +90,16 @@
         if (!mInitialized) {
             return;
         }
+        // Don't restart InputMethodService that claims it does not use resources at all.
+        boolean neverReset = android.content.res.Flags.handleAllConfigChanges()
+                && (mHandledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0;
+
         final int diff = mLastKnownConfig != null
                 ? mLastKnownConfig.diffPublicOnly(newConfig) : CONFIG_CHANGED;
         // If the new config is the same as the config this Service is already running with,
         // then don't bother calling resetStateForNewConfiguration.
         final int unhandledDiff = (diff & ~mHandledConfigChanges);
-        if (unhandledDiff != 0) {
+        if (unhandledDiff != 0 && !neverReset) {
             resetStateForNewConfigurationRunner.run();
         }
         if (diff != 0) {
diff --git a/core/java/android/tracing/flags.aconfig b/core/java/android/tracing/flags.aconfig
index be60c25..04dba46 100644
--- a/core/java/android/tracing/flags.aconfig
+++ b/core/java/android/tracing/flags.aconfig
@@ -46,3 +46,11 @@
     is_fixed_read_only: true
     bug: "323165543"
 }
+
+flag {
+    name: "client_side_proto_logging"
+    namespace: "windowing_tools"
+    description: "Add support for client side protologging"
+    is_fixed_read_only: true
+    bug: "352538294"
+}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index cb5a885..e5be531 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -53,6 +53,7 @@
 import android.view.IWindow;
 import android.view.IWindowSession;
 import android.view.IWindowSessionCallback;
+import android.view.KeyboardShortcutGroup;
 import android.view.KeyEvent;
 import android.view.InputEvent;
 import android.view.InsetsState;
@@ -1095,4 +1096,11 @@
 
     boolean transferTouchGesture(in InputTransferToken transferFromToken,
             in InputTransferToken transferToToken);
+
+    /**
+     * Request the application launch keyboard shortcuts the system has defined.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will handle the shortcut.
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
 }
diff --git a/core/java/android/view/KeyboardShortcutGroup.aidl b/core/java/android/view/KeyboardShortcutGroup.aidl
new file mode 100644
index 0000000..6f219db
--- /dev/null
+++ b/core/java/android/view/KeyboardShortcutGroup.aidl
@@ -0,0 +1,3 @@
+package android.view;
+
+@JavaOnlyStableParcelable parcelable KeyboardShortcutGroup;
diff --git a/core/java/android/view/KeyboardShortcutInfo.java b/core/java/android/view/KeyboardShortcutInfo.java
index 3f6fd64..3f49bf3 100644
--- a/core/java/android/view/KeyboardShortcutInfo.java
+++ b/core/java/android/view/KeyboardShortcutInfo.java
@@ -81,12 +81,29 @@
      *     {@link KeyEvent#META_SYM_ON}.
      */
     public KeyboardShortcutInfo(CharSequence label, char baseCharacter, int modifiers) {
+        this(label, null, baseCharacter, modifiers);
+    }
+
+    /**
+     * @param label The label that identifies the action performed by this shortcut.
+     * @param icon An icon that identifies the action performed by this shortcut.
+     * @param baseCharacter The character that triggers the shortcut.
+     * @param modifiers The set of modifiers that, combined with the key, trigger the shortcut.
+     *     These should be a combination of {@link KeyEvent#META_CTRL_ON},
+     *     {@link KeyEvent#META_SHIFT_ON}, {@link KeyEvent#META_META_ON},
+     *     {@link KeyEvent#META_ALT_ON}, {@link KeyEvent#META_FUNCTION_ON} and
+     *     {@link KeyEvent#META_SYM_ON}.
+     *
+     * @hide
+     */
+    public KeyboardShortcutInfo(
+            CharSequence label, @Nullable Icon icon, char baseCharacter, int modifiers) {
         mLabel = label;
         checkArgument(baseCharacter != MIN_VALUE);
         mBaseCharacter = baseCharacter;
         mKeycode = KeyEvent.KEYCODE_UNKNOWN;
         mModifiers = modifiers;
-        mIcon = null;
+        mIcon = icon;
     }
 
     private KeyboardShortcutInfo(Parcel source) {
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index 117b200..a806bd2 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -295,16 +295,7 @@
         }
 
         sCallStackDebuggingInitialized = true;
-        sCallStackDebuggingMatchCall =
-                SystemProperties.get("persist.wm.debug.sc.tx.log_match_call", null)
-                        .toLowerCase();
-        sCallStackDebuggingMatchName =
-                SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null)
-                        .toLowerCase();
-        sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
-        // Only enable stack debugging if any of the match filters are set
-        sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty()
-                || !sCallStackDebuggingMatchName.isEmpty();
+        updateCallStackDebuggingParams();
         if (sCallStackDebuggingEnabled) {
             Log.d(TAG, "Enabling transaction call stack debugging:"
                     + " matchCall=" + sCallStackDebuggingMatchCall
@@ -325,6 +316,11 @@
     final void checkCallStackDebugging(@NonNull String call,
             @Nullable SurfaceControl.Transaction tx, @Nullable SurfaceControl sc,
             @Nullable String details) {
+
+        if (sCallStackDebuggingInitialized && sCallStackDebuggingEnabled) {
+            updateCallStackDebuggingParams();
+        }
+
         if (!sCallStackDebuggingEnabled) {
             return;
         }
@@ -356,6 +352,22 @@
     }
 
     /**
+     * Updates the call stack debugging params from the system properties.
+     */
+    private static void updateCallStackDebuggingParams() {
+        sCallStackDebuggingMatchCall =
+                SystemProperties.get("persist.wm.debug.sc.tx.log_match_call", null)
+                        .toLowerCase();
+        sCallStackDebuggingMatchName =
+                SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null)
+                        .toLowerCase();
+        sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
+        // Only enable stack debugging if any of the match filters are set
+        sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty()
+                || !sCallStackDebuggingMatchName.isEmpty();
+    }
+
+    /**
      * Tests whether the given surface control name/method call matches the filters set for the
      * call stack debugging.
      */
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index a6c6c18..e5b17c8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -128,7 +128,6 @@
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme;
-import static com.android.window.flags.Flags.activityWindowInfoFlag;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
 import static com.android.window.flags.Flags.enableCaptionCompatInsetForceConsumption;
 import static com.android.window.flags.Flags.insetsControlChangedItem;
@@ -1374,9 +1373,6 @@
      */
     public void setActivityConfigCallback(@Nullable ActivityConfigCallback callback) {
         mActivityConfigCallback = callback;
-        if (!activityWindowInfoFlag()) {
-            return;
-        }
         if (callback == null) {
             mPendingActivityWindowInfo = null;
             mLastReportedActivityWindowInfo = null;
@@ -9360,7 +9356,7 @@
 
             onClientWindowFramesChanged(mTmpFrames);
 
-            if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) {
+            if (mPendingActivityWindowInfo != null) {
                 final ActivityWindowInfo outInfo = mRelayoutResult.activityWindowInfo;
                 if (outInfo != null) {
                     mPendingActivityWindowInfo.set(outInfo);
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 18006bb..14978ed 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1668,6 +1668,15 @@
     public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId);
 
     /**
+     * Request the application launch keyboard shortcuts the system has defined.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will handle the shortcut.
+     *
+     * @hide
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
+
+    /**
      * Request for ime's keyboard shortcuts to be retrieved asynchronously.
      *
      * @param receiver The callback to be triggered when the result is ready.
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index b667427..330e46a 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -237,6 +237,16 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        try {
+            return WindowManagerGlobal.getWindowManagerService()
+                    .getApplicationLaunchKeyboardShortcuts(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
     public void requestImeKeyboardShortcuts(
             final KeyboardShortcutsReceiver receiver, int deviceId) {
         IResultReceiver resultReceiver = new IResultReceiver.Stub() {
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index d4c3fbe..15f1258 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -328,6 +328,7 @@
      * only occupies a portion of Task bounds.
      * @hide
      */
+    // TODO(b/287582673): cleanup
     public boolean isActivityEmbedded(@NonNull IBinder activityToken) {
         try {
             return getController().isActivityEmbedded(activityToken);
diff --git a/core/java/android/window/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java
index cc22576..253337b 100644
--- a/core/java/android/window/TransitionRequestInfo.java
+++ b/core/java/android/window/TransitionRequestInfo.java
@@ -471,7 +471,7 @@
                 "pipTask = " + mPipTask + ", " +
                 "remoteTransition = " + mRemoteTransition + ", " +
                 "displayChange = " + mDisplayChange + ", " +
-                "flags = " + mFlags + ", " +
+                "flags = " + Integer.toHexString(mFlags) + ", " +
                 "debugId = " + mDebugId +
         " }";
     }
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index f732929..1362f7b 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -168,4 +168,11 @@
     namespace: "lse_desktop_experience"
     description: "Whether to enable back navigation treatment in desktop windowing."
     bug: "350421096"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "enable_desktop_windowing_app_handle_education"
+    namespace: "lse_desktop_experience"
+    description: "Enables desktop windowing app handle education"
+    bug: "348208342"
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 8fd525c..5397e91 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -208,7 +208,7 @@
 
 flag {
   name: "enforce_shell_thread_model"
-  namespace: "windowing_frentend"
+  namespace: "windowing_frontend"
   description: "Crash the shell process if someone calls in from the wrong thread"
   bug: "351189446"
   is_fixed_read_only: true
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 2b096ea..3e6f18e 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -188,9 +188,17 @@
      */
     public static final int CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU = 112;
 
+    /** Track Launcher Keyboard Quick Switch View opening animation */
+    public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN = 113;
+
+    /** Track Launcher Keyboard Quick Switch View closing animation */
+    public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE = 114;
+
+    /** Track launching an app through the Launcher Keyboard Quick Switch View */
+    public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH = 115;
 
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
-    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
+    @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
 
     /** @hide */
     @IntDef({
@@ -294,7 +302,10 @@
             CUJ_DESKTOP_MODE_EXIT_MODE,
             CUJ_DESKTOP_MODE_MINIMIZE_WINDOW,
             CUJ_DESKTOP_MODE_DRAG_WINDOW,
-            CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP
+            CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+            CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN,
+            CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE,
+            CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {}
@@ -409,6 +420,9 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_DRAG_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_DRAG_WINDOW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
     }
 
     private Cuj() {
@@ -629,6 +643,12 @@
                 return "DESKTOP_MODE_DRAG_WINDOW";
             case CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP:
                 return "STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP";
+            case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN:
+                return "LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN";
+            case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE:
+                return "LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE";
+            case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH:
+                return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 30fa4f1..2ad6651 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -12,7 +12,7 @@
 
 flag {
     name: "use_transaction_codes_for_unknown_methods"
-    namespace: "dropbox"
+    namespace: "stability"
     description: "Use transaction codes when the method names is unknown"
     bug: "350041302"
     is_fixed_read_only: true
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 50fb8d5..652cba7 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -98,6 +98,7 @@
             this::onTracingFlush,
             this::onTracingInstanceStop
     );
+    @Nullable
     private final ProtoLogViewerConfigReader mViewerConfigReader;
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
     private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
@@ -126,7 +127,7 @@
     }
 
     public PerfettoProtoLogImpl(
-            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
             Runnable cacheUpdater
     ) {
         this(viewerConfigInputStreamProvider,
@@ -136,8 +137,8 @@
 
     @VisibleForTesting
     public PerfettoProtoLogImpl(
-            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
-            ProtoLogViewerConfigReader viewerConfigReader,
+            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @Nullable ProtoLogViewerConfigReader viewerConfigReader,
             Runnable cacheUpdater
     ) {
         Producer.init(InitArguments.DEFAULTS);
@@ -209,7 +210,9 @@
      * @return status code
      */
     public int startLoggingToLogcat(String[] groups, ILogger logger) {
-        mViewerConfigReader.loadViewerConfig(logger);
+        if (mViewerConfigReader != null) {
+            mViewerConfigReader.loadViewerConfig(groups, logger);
+        }
         return setTextLogging(true, logger, groups);
     }
 
@@ -220,7 +223,9 @@
      * @return status code
      */
     public int stopLoggingToLogcat(String[] groups, ILogger logger) {
-        mViewerConfigReader.unloadViewerConfig();
+        if (mViewerConfigReader != null) {
+            mViewerConfigReader.unloadViewerConfig(groups, logger);
+        }
         return setTextLogging(false, logger, groups);
     }
 
@@ -262,7 +267,9 @@
                 return -1;
             }
             case "enable-text" -> {
-                mViewerConfigReader.loadViewerConfig(logger);
+                if (mViewerConfigReader != null) {
+                    mViewerConfigReader.loadViewerConfig(groups, logger);
+                }
                 return setTextLogging(true, logger, groups);
             }
             case "disable-text" -> {
@@ -420,7 +427,12 @@
 
     private void logToLogcat(String tag, LogLevel level, Message message,
             @Nullable Object[] args) {
-        String messageString = message.getMessage(mViewerConfigReader);
+        String messageString;
+        if (mViewerConfigReader == null) {
+            messageString = message.getMessage();
+        } else {
+            messageString = message.getMessage(mViewerConfigReader);
+        }
 
         if (messageString == null) {
             StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE");
@@ -827,7 +839,11 @@
             return mMessageMask;
         }
 
-        private String getMessage(ProtoLogViewerConfigReader viewerConfigReader) {
+        private String getMessage() {
+            return mMessageString;
+        }
+
+        private String getMessage(@NonNull ProtoLogViewerConfigReader viewerConfigReader) {
             if (mMessageString != null) {
                 return mMessageString;
             }
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index b7b2424..bb6c8b7 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -1,20 +1,32 @@
 package com.android.internal.protolog;
 
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
+
 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
 
-import android.util.ArrayMap;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+import android.util.LongSparseArray;
 import android.util.proto.ProtoInputStream;
 
 import com.android.internal.protolog.common.ILogger;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
 
 public class ProtoLogViewerConfigReader {
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
-    private Map<Long, String> mLogMessageMap = null;
+    private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>();
+    private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>();
 
     public ProtoLogViewerConfigReader(
             ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
@@ -26,39 +38,62 @@
      * or the viewer config is not loaded into memory.
      */
     public synchronized String getViewerString(long messageHash) {
-        if (mLogMessageMap != null) {
-            return mLogMessageMap.get(messageHash);
-        } else {
-            return null;
-        }
+        return mLogMessageMap.get(messageHash);
+    }
+
+    public synchronized void loadViewerConfig(String[] groups) {
+        loadViewerConfig(groups, (message) -> {});
     }
 
     /**
      * Loads the viewer config into memory. No-op if already loaded in memory.
      */
-    public synchronized void loadViewerConfig(ILogger logger) {
-        if (mLogMessageMap != null) {
-            return;
-        }
+    public synchronized void loadViewerConfig(String[] groups, @NonNull ILogger logger) {
+        for (String group : groups) {
+            if (mGroupHashes.containsKey(group)) {
+                continue;
+            }
 
-        try {
-            doLoadViewerConfig();
-            logger.log("Loaded " + mLogMessageMap.size() + " log definitions");
-        } catch (IOException e) {
-            logger.log("Unable to load log definitions: "
-                    + "IOException while processing viewer config" + e);
+            try {
+                Map<Long, String> mappings = loadViewerConfigMappingForGroup(group);
+                mGroupHashes.put(group, mappings.keySet());
+                for (Long key : mappings.keySet()) {
+                    mLogMessageMap.put(key, mappings.get(key));
+                }
+
+                logger.log("Loaded " + mLogMessageMap.size() + " log definitions");
+            } catch (IOException e) {
+                logger.log("Unable to load log definitions: "
+                        + "IOException while processing viewer config" + e);
+            }
         }
     }
 
+    public synchronized void unloadViewerConfig(String[] groups) {
+        unloadViewerConfig(groups, (message) -> {});
+    }
+
     /**
      * Unload the viewer config from memory.
      */
-    public synchronized void unloadViewerConfig() {
-        mLogMessageMap = null;
+    public synchronized void unloadViewerConfig(String[] groups, @NonNull ILogger logger) {
+        for (String group : groups) {
+            if (!mGroupHashes.containsKey(group)) {
+                continue;
+            }
+
+            final Set<Long> hashes = mGroupHashes.get(group);
+            for (Long hash : hashes) {
+                logger.log("Unloading viewer config hash " + hash);
+                mLogMessageMap.remove(hash);
+            }
+        }
     }
 
-    private void doLoadViewerConfig() throws IOException {
-        mLogMessageMap = new ArrayMap<>();
+    private Map<Long, String> loadViewerConfigMappingForGroup(String group) throws IOException {
+        Long targetGroupId = loadGroupId(group);
+
+        final Map<Long, String> hashesForGroup = new TreeMap<>();
         final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
 
         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
@@ -67,6 +102,7 @@
 
                 long messageId = 0;
                 String message = null;
+                int groupId = 0;
                 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
                     switch (pis.getFieldNumber()) {
                         case (int) MESSAGE_ID:
@@ -75,9 +111,16 @@
                         case (int) MESSAGE:
                             message = pis.readString(MESSAGE);
                             break;
+                        case (int) GROUP_ID:
+                            groupId = pis.readInt(GROUP_ID);
+                            break;
                     }
                 }
 
+                if (groupId == 0) {
+                    throw new IOException("Failed to get group id");
+                }
+
                 if (messageId == 0) {
                     throw new IOException("Failed to get message id");
                 }
@@ -86,10 +129,45 @@
                     throw new IOException("Failed to get message string");
                 }
 
-                mLogMessageMap.put(messageId, message);
+                if (groupId == targetGroupId) {
+                    hashesForGroup.put(messageId, message);
+                }
 
                 pis.end(inMessageToken);
             }
         }
+
+        return hashesForGroup;
+    }
+
+    private Long loadGroupId(String group) throws IOException {
+        final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
+
+        while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (pis.getFieldNumber() == (int) GROUPS) {
+                final long inMessageToken = pis.start(GROUPS);
+
+                long groupId = 0;
+                String groupName = null;
+                while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    switch (pis.getFieldNumber()) {
+                        case (int) ID:
+                            groupId = pis.readInt(ID);
+                            break;
+                        case (int) NAME:
+                            groupName = pis.readString(NAME);
+                            break;
+                    }
+                }
+
+                if (Objects.equals(groupName, group)) {
+                    return groupId;
+                }
+
+                pis.end(inMessageToken);
+            }
+        }
+
+        throw new RuntimeException("Group " + group + "not found in viewer config");
     }
 }
diff --git a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
index b75d545..eddf1d2 100644
--- a/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
+++ b/core/proto/android/server/inputmethod/inputmethodmanagerservice.proto
@@ -47,4 +47,5 @@
     optional int32 ime_window_visibility = 22;
     optional bool show_ime_with_hard_keyboard = 23;
     optional bool accessibility_requesting_no_soft_keyboard = 24;
-}
\ No newline at end of file
+    optional bool concurrent_multi_user_mode_enabled = 25;
+}
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index f94c8ab..2e3dbda 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1052,6 +1052,19 @@
         <!-- The font weight adjustment value has changed. Used to reflect the user increasing font
              weight. -->
         <flag name="fontWeightAdjustment" value="0x10000000" />
+        <!-- The assets paths have changed. For example a runtime overlay is installed and enabled.
+             Corresponds to {@link android.content.pm.ActivityInfo#CONFIG_ASSETS_PATHS}. -->
+        <flag name="assetsPaths" value="0x80000000" />
+        <!-- This is probably not the flag you want, the resources compiler supports a less
+             dangerous version of it, 'allKnown', that only suppresses all currently existing
+             configuration change restarts depending on your target SDK rather than whatever the
+             latest SDK supports, allowing the application to work with resources on future Platform
+             versions.
+             Activity doesn't use Android Resources at all and doesn't need to be restarted on any
+             configuration changes. This overrides all other flags, and this is recommended to be
+             used individually. Corresponds to
+             {@link android.content.pm.ActivityInfo#CONFIG_RESOURCES_UNUSED}. -->
+        <flag name="resourcesUnused" value="0x8000000" />
     </attr>
 
     <!-- Indicate that the activity can be launched as the embedded child of another
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6b71f97..46b15416 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6534,4 +6534,23 @@
     <string name="bg_user_sound_notification_button_mute">Mute</string>
     <!-- Notification text to mute the sound from the background user [CHAR LIMIT=NOTIF_BODY]-->
     <string name="bg_user_sound_notification_message">Tap to mute sound</string>
+
+    <!-- User visible title for the keyboard shortcut that takes the user to the browser app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_browser">Browser</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the contacts app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_contacts">Contacts</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the email app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_email">Email</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the SMS messaging app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_sms">SMS</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the music app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_music">Music</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the calendar app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_calendar">Calendar</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the calculator app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_calculator">Calculator</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the maps app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_maps">Maps</string>
+    <!-- User visible title for the keyboard shortcut group containing system-wide application launch shortcuts. [CHAR-LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications">Applications</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d25f59d..c50b961 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5561,4 +5561,15 @@
   <java-symbol type="string" name="bg_user_sound_notification_button_switch_user" />
   <java-symbol type="string" name="bg_user_sound_notification_button_mute" />
   <java-symbol type="string" name="bg_user_sound_notification_message" />
+
+  <!-- Keyboard Shortcut default category names. -->
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_browser" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_calculator" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_calendar" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_contacts" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_email" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_maps" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_music" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications_sms" />
+  <java-symbol type="string" name="keyboard_shortcut_group_applications" />
 </resources>
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index f87a9e2..e8a0762 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -25,8 +25,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static com.android.window.flags.Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -810,7 +808,6 @@
 
     @Test
     public void testActivityWindowInfoChanged_activityLaunch() {
-        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);
         ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                 mActivityWindowInfoListener);
 
@@ -825,7 +822,6 @@
 
     @Test
     public void testActivityWindowInfoChanged_activityRelaunch() {
-        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);
         ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                 mActivityWindowInfoListener);
 
@@ -866,7 +862,6 @@
 
     @Test
     public void testActivityWindowInfoChanged_activityConfigurationChanged() {
-        mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG);
         ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener(
                 mActivityWindowInfoListener);
 
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index 0b270d4..d2a444f 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -53,8 +53,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.window.flags.Flags;
-
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -167,8 +165,6 @@
 
     @Test
     public void testActivityWindowInfoChangedListener() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
-
         mController.registerActivityWindowInfoChangedListener(mActivityWindowInfoListener);
         final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo();
         activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000),
diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
index 064439e..6998c32 100644
--- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
+++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -74,4 +75,33 @@
         assertFalse("IME shouldn't restart since it handles configChanges",
                 didReset.get());
     }
+
+    @Test
+    @RequiresFlagsEnabled(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+    public void testShouldImeRestart_handleResourceUnused() throws Exception {
+        Configuration config = mContext.getResources().getConfiguration();
+        mImsConfigTracker.onInitialize(0 /* handledConfigChanges */);
+        mImsConfigTracker.onBindInput(mContext.getResources());
+        Configuration newConfig = new Configuration(config);
+
+        final AtomicBoolean didReset = new AtomicBoolean();
+        Runnable resetStateRunner = () -> didReset.set(true);
+
+        mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+        assertFalse("IME shouldn't restart if config hasn't changed",
+                didReset.get());
+
+        // Screen density changed but IME doesn't handle configChanges
+        newConfig.densityDpi = 99;
+        mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+        assertTrue("IME should restart for unhandled configChanges",
+                didReset.get());
+
+        didReset.set(false);
+        // opt-in IME to handle all configuration changes.
+        mImsConfigTracker.setHandledConfigChanges(ActivityInfo.CONFIG_RESOURCES_UNUSED);
+        mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+        assertFalse("IME shouldn't restart since it handles configChanges",
+                didReset.get());
+    }
 }
diff --git a/core/tests/overlaytests/handle_config_change/Android.bp b/core/tests/overlaytests/handle_config_change/Android.bp
new file mode 100644
index 0000000..2b31d0a
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_resources",
+}
+
+java_test_host {
+    name: "HandleConfigChangeHostTests",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "tradefed",
+        "compatibility-host-util",
+    ],
+    static_libs: [
+        "compatibility-host-util-axt",
+        "flag-junit-host",
+        "android.content.res.flags-aconfig-java-host",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    // All APKs required by the tests
+    data: [
+        ":OverlayResApp",
+    ],
+    per_testcase_directory: true,
+}
diff --git a/core/tests/overlaytests/handle_config_change/AndroidTest.xml b/core/tests/overlaytests/handle_config_change/AndroidTest.xml
new file mode 100644
index 0000000..05ea036
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<configuration description="Config for the handle config change test cases">
+    <option name="test-tag" value="HandleConfigChangeHostTests" />
+    <option name="test-suite-tag" value="apct" />
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="force-skip-system-props" value="true" />
+        <option name="set-global-setting" key="verifier_engprod" value="1" />
+        <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+        <option name="restore-settings" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="OverlayResApp.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.HostTest">
+        <option name="class" value="com.android.overlaytest.HandleConfigChangeHostTests" />
+    </test>
+</configuration>
diff --git a/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java
new file mode 100644
index 0000000..e91716a
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.overlaytest;
+
+import android.content.res.Flags;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.host.HostFlagsValueProvider;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class HandleConfigChangeHostTests extends BaseHostJUnit4Test {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            HostFlagsValueProvider.createCheckFlagsRule(this::getDevice);
+    private static final String DEVICE_TEST_PKG1 = "com.android.overlaytest.overlayresapp";
+    private static final String DEVICE_TEST_CLASS = "OverlayResTest";
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+    public void testOverlayRes() throws Exception {
+        runDeviceTests(DEVICE_TEST_PKG1, DEVICE_TEST_PKG1 + "." + DEVICE_TEST_CLASS,
+                "overlayRes_onConfigurationChanged");
+    }
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp
new file mode 100644
index 0000000..e0f1012
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp
@@ -0,0 +1,43 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "OverlayResApp",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    certificate: "platform",
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.rules",
+        "androidx.test.core",
+        "compatibility-device-util-axt",
+        "truth",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+
+    manifest: "AndroidManifest.xml",
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml
new file mode 100644
index 0000000..617e879
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.overlaytest.overlayresapp"
+    xmlns:tools="http://schemas.android.com/tools">
+
+  <application>
+    <uses-library android:name="android.test.runner" />
+    <activity
+        android:name=".OverlayResActivity"
+        android:exported="false"
+        android:configChanges="assetsPaths">
+    </activity>
+  </application>
+
+  <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                   android:targetPackage="com.android.overlaytest.overlayresapp" />
+
+</manifest>
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml
new file mode 100644
index 0000000..493333f
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <integer name="test_integer">0</integer>
+</resources>
\ No newline at end of file
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml
new file mode 100644
index 0000000..72820d3
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+  <string name="app_name">My Application</string>
+  <string name="test_string">Test String</string>
+</resources>
\ No newline at end of file
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java
new file mode 100644
index 0000000..96143ae
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.overlaytest.overlayresapp;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A test activity to verify that the assets paths configuration changes are received if the
+ * overlay targeting state is changed.
+ */
+public class OverlayResActivity extends Activity {
+    private Runnable mConfigurationChangedCallback;
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        final Runnable callback = mConfigurationChangedCallback;
+        if (callback != null) {
+            callback.run();
+        }
+    }
+
+    /** Registers the callback of onConfigurationChanged. */
+    public void setConfigurationChangedCallback(Runnable callback) {
+        mConfigurationChangedCallback = callback;
+    }
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java
new file mode 100644
index 0000000..1c37719
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 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.overlaytest.overlayresapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.om.FabricatedOverlay;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayManager;
+import android.content.om.OverlayManagerTransaction;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.util.TypedValue;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class OverlayResTest {
+    // Default timeout value
+    private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
+    private static final String TEST_OVERLAY_NAME = "Test";
+    private static final String TEST_RESOURCE_INTEGER = "integer/test_integer";
+    private static final String TEST_RESOURCE_STRING = "string/test_string";
+    private static final int TEST_INTEGER = 0;
+    private static final int TEST_FRRO_INTEGER = 1;
+    private static final String TEST_STRING = "Test String";
+    private static final String TEST_FRRO_STRING = "FRRO Test String";
+    private OverlayResActivity mActivity;
+    private Context mContext;
+    private OverlayManager mOverlayManager;
+    private int mUserId;
+    private UserHandle mUserHandle;
+
+    @Rule
+    public ActivityScenarioRule<OverlayResActivity> mActivityScenarioRule =
+            new ActivityScenarioRule<>(OverlayResActivity.class);
+
+    @Before
+    public void setUp() {
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            assertThat(activity).isNotNull();
+            mActivity = activity;
+        });
+        mContext = mActivity.getApplicationContext();
+        mOverlayManager = mContext.getSystemService(OverlayManager.class);
+        mUserId = UserHandle.myUserId();
+        mUserHandle = UserHandle.of(mUserId);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        final OverlayManagerTransaction.Builder cleanUp = new OverlayManagerTransaction.Builder();
+        mOverlayManager.getOverlayInfosForTarget(mContext.getPackageName(), mUserHandle).forEach(
+                info -> {
+                    if (info.isFabricated()) {
+                        cleanUp.unregisterFabricatedOverlay(info.getOverlayIdentifier());
+                    }
+                });
+        mOverlayManager.commit(cleanUp.build());
+
+    }
+
+    @Test
+    public void overlayRes_onConfigurationChanged() throws Exception {
+        final CountDownLatch latch1 = new CountDownLatch(1);
+        mActivity.setConfigurationChangedCallback(() -> {
+            Resources r = mActivity.getApplicationContext().getResources();
+            assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_FRRO_INTEGER);
+            assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_FRRO_STRING);
+            latch1.countDown();
+        });
+
+        // Create and enable FRRO
+        final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
+                mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
+                .setResourceValue(TEST_RESOURCE_INTEGER, TypedValue.TYPE_INT_DEC, TEST_FRRO_INTEGER)
+                .setResourceValue(TEST_RESOURCE_STRING, TypedValue.TYPE_STRING, TEST_FRRO_STRING)
+                .build();
+
+        mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+                .registerFabricatedOverlay(overlay)
+                .build());
+
+        OverlayInfo info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle);
+        assertNotNull(info);
+        assertThat(info.isEnabled()).isFalse();
+
+        mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+                .setEnabled(overlay.getIdentifier(), true, mUserId)
+                .build());
+
+        info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle);
+        assertNotNull(info);
+        assertThat(info.isEnabled()).isTrue();
+
+        if (!latch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Fail to wait configuration changes for build and enabling frro, "
+                    + "onConfigurationChanged() has not been invoked.");
+        }
+
+        final CountDownLatch latch2 = new CountDownLatch(1);
+        mActivity.setConfigurationChangedCallback(() -> {
+            Resources r = mActivity.getApplicationContext().getResources();
+            assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_INTEGER);
+            assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_STRING);
+            latch2.countDown();
+        });
+
+        // unregister FRRO
+        mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+                .unregisterFabricatedOverlay(overlay.getIdentifier())
+                .build());
+
+        if (!latch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Fail to wait configuration changes after unregister frro,"
+                    + " onConfigurationChanged() has not been invoked.");
+        }
+    }
+}
diff --git a/core/tests/resourceflaggingtests/OWNERS b/core/tests/resourceflaggingtests/OWNERS
new file mode 100644
index 0000000..10950a1
--- /dev/null
+++ b/core/tests/resourceflaggingtests/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/app/RESOURCES_OWNERS
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 3261a37..b120723 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -204,11 +204,9 @@
 
     /** Listener registered to {@link ClientTransactionListenerController}. */
     @GuardedBy("mLock")
-    @Nullable
+    @NonNull
     private final BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener =
-            Flags.activityWindowInfoFlag()
-                    ? this::onActivityWindowInfoChanged
-                    : null;
+            this::onActivityWindowInfoChanged;
 
     private final Handler mHandler;
     private final MainThreadExecutor mExecutor;
@@ -3096,20 +3094,14 @@
     public boolean isActivityEmbedded(@NonNull Activity activity) {
         Objects.requireNonNull(activity);
         synchronized (mLock) {
-            if (Flags.activityWindowInfoFlag()) {
-                final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity);
-                return activityWindowInfo != null && activityWindowInfo.isEmbedded();
-            }
-            return mPresenter.isActivityEmbedded(activity.getActivityToken());
+            final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity);
+            return activityWindowInfo != null && activityWindowInfo.isEmbedded();
         }
     }
 
     @Override
     public void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor,
             @NonNull Consumer<EmbeddedActivityWindowInfo> callback) {
-        if (!Flags.activityWindowInfoFlag()) {
-            return;
-        }
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
         synchronized (mLock) {
@@ -3123,9 +3115,6 @@
 
     @Override
     public void clearEmbeddedActivityWindowInfoCallback() {
-        if (!Flags.activityWindowInfoFlag()) {
-            return;
-        }
         synchronized (mLock) {
             if (mEmbeddedActivityWindowInfoCallback == null) {
                 return;
@@ -3146,9 +3135,6 @@
     @Nullable
     @Override
     public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) {
-        if (!Flags.activityWindowInfoFlag()) {
-            return null;
-        }
         synchronized (mLock) {
             final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity);
             return activityWindowInfo != null
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index efeec82..99c0ee2 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -103,8 +103,6 @@
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
 import androidx.window.extensions.layout.WindowLayoutInfo;
 
-import com.android.window.flags.Flags;
-
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -1557,8 +1555,6 @@
 
     @Test
     public void testIsActivityEmbedded() {
-        mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
-
         assertFalse(mSplitController.isActivityEmbedded(mActivity));
 
         doReturn(true).when(mActivityWindowInfo).isEmbedded();
@@ -1568,8 +1564,6 @@
 
     @Test
     public void testGetEmbeddedActivityWindowInfo() {
-        mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
-
         final boolean isEmbedded = true;
         final Rect taskBounds = new Rect(0, 0, 1000, 2000);
         final Rect activityStackBounds = new Rect(0, 0, 500, 2000);
@@ -1584,8 +1578,6 @@
 
     @Test
     public void testSetEmbeddedActivityWindowInfoCallback() {
-        mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
-
         final ClientTransactionListenerController controller = ClientTransactionListenerController
                 .getInstance();
         spyOn(controller);
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index dbcad8a..a00d003 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -224,7 +224,6 @@
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "//frameworks/libs/systemui:iconloader_base",
         "com_android_wm_shell_flags_lib",
-        "com.android.window.flags.window-aconfig-java",
         "WindowManager-Shell-proto",
         "WindowManager-Shell-shared",
         "perfetto_trace_java_protos",
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 3ff40e0..56ea7c2 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -113,16 +113,6 @@
 }
 
 flag {
-    name: "animate_bubble_size_change"
-    namespace: "multitasking"
-    description: "Turns on the animation for bubble bar icons size change"
-    bug: "335575529"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "enable_taskbar_on_phones"
     namespace: "multitasking"
     description: "Enables taskbar on phones"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 419d5c0..864f7cd 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -19,6 +19,10 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="@dimen/desktop_mode_handle_menu_width"
     android:layout_height="wrap_content"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:paddingBottom="@dimen/desktop_mode_handle_menu_pill_elevation"
+    android:paddingRight="@dimen/desktop_mode_handle_menu_pill_elevation"
     android:orientation="vertical">
 
     <LinearLayout
@@ -27,7 +31,7 @@
         android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_margin_top"
         android:layout_marginStart="1dp"
-        android:elevation="1dp"
+        android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
         android:orientation="horizontal"
         android:background="@drawable/desktop_mode_decor_handle_menu_background"
         android:gravity="center_vertical">
@@ -73,7 +77,7 @@
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
         android:layout_marginStart="1dp"
         android:orientation="horizontal"
-        android:elevation="1dp"
+        android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
         android:background="@drawable/desktop_mode_decor_handle_menu_background"
         android:gravity="center_vertical">
 
@@ -124,7 +128,7 @@
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
         android:layout_marginStart="1dp"
         android:orientation="vertical"
-        android:elevation="1dp"
+        android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
         android:background="@drawable/desktop_mode_decor_handle_menu_background">
 
         <Button
@@ -143,7 +147,7 @@
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
         android:layout_marginStart="1dp"
         android:orientation="vertical"
-        android:elevation="1dp"
+        android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
         android:background="@drawable/desktop_mode_decor_handle_menu_background">
 
         <Button
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index d143263..269a586 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -495,9 +495,16 @@
     <!-- The radius of the Maximize menu shadow. -->
     <dimen name="desktop_mode_maximize_menu_shadow_radius">8dp</dimen>
 
-    <!-- The width of the handle menu in desktop mode. -->
+    <!-- The width of the handle menu in desktop mode.  -->
     <dimen name="desktop_mode_handle_menu_width">216dp</dimen>
 
+    <!-- The maximum height of the handle menu in desktop mode. Four pills (52dp each) plus 2dp
+        spacing between them plus 4dp top padding. -->
+    <dimen name="desktop_mode_handle_menu_height">218dp</dimen>
+
+    <!-- The elevation set on the handle menu pills. -->
+    <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen>
+
     <!-- The height of the handle menu's "App Info" pill in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_app_info_pill_height">52dp</dimen>
 
@@ -510,8 +517,8 @@
     <!-- The height of the handle menu's "Open in browser" pill in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen>
 
-    <!-- The height of the handle menu in desktop mode. -->
-    <dimen name="desktop_mode_handle_menu_height">380dp</dimen>
+    <!-- The margin between pills of the handle menu in desktop mode. -->
+    <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen>
 
     <!-- The top margin of the handle menu in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_margin_top">4dp</dimen>
@@ -519,9 +526,6 @@
     <!-- The start margin of the handle menu in desktop mode. -->
     <dimen name="desktop_mode_handle_menu_margin_start">6dp</dimen>
 
-    <!-- The margin between pills of the handle menu in desktop mode. -->
-    <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen>
-
     <!-- The radius of the caption menu corners. -->
     <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index ece0271..8467e97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -422,6 +422,12 @@
     @VisibleForTesting
     public void onThresholdCrossed() {
         mThresholdCrossed = true;
+        // There was no focus window when calling startBackNavigation, still pilfer pointers so
+        // the next focus window won't receive motion events.
+        if (mBackNavigationInfo == null) {
+            tryPilferPointers();
+            return;
+        }
         // Dispatch onBackStarted, only to app callbacks.
         // System callbacks will receive onBackStarted when the remote animation starts.
         final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
@@ -542,6 +548,7 @@
         if (backNavigationInfo == null) {
             ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
             cancelLatencyTracking();
+            tryPilferPointers();
             return;
         }
         final int backType = backNavigationInfo.getType();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index eeceaa9..45feff5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -245,7 +245,10 @@
         return new CaptionWindowDecorViewModel(
                 context,
                 mainHandler,
+                mainExecutor,
                 mainChoreographer,
+                windowManager,
+                shellInit,
                 taskOrganizer,
                 displayController,
                 rootTaskDisplayAreaOrganizer,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 240cf3b..037fbb2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -38,12 +38,10 @@
 import com.android.wm.shell.common.pip.PipPerfHintController;
 import com.android.wm.shell.common.pip.PipSnapAlgorithm;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
-import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.onehanded.OneHandedController;
-import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
 import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
@@ -79,7 +77,7 @@
 public abstract class Pip1Module {
     @WMSingleton
     @Provides
-    static Optional<Pip> providePip1(Context context,
+    static Optional<PipController.PipImpl> providePip1(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
             ShellController shellController,
@@ -104,20 +102,16 @@
             TabletopModeController pipTabletopController,
             Optional<OneHandedController> oneHandedController,
             @ShellMainThread ShellExecutor mainExecutor) {
-        if (PipUtils.isPip2ExperimentEnabled()) {
-            return Optional.empty();
-        } else {
-            return Optional.ofNullable(PipController.create(
-                    context, shellInit, shellCommandHandler, shellController,
-                    displayController, pipAnimationController, pipAppOpsListener,
-                    pipBoundsAlgorithm,
-                    pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState,
-                    pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
-                    pipTransitionState, pipTouchHandler, pipTransitionController,
-                    windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
-                    displayInsetsController, pipTabletopController, oneHandedController,
-                    mainExecutor));
-        }
+        return Optional.ofNullable(PipController.create(
+                context, shellInit, shellCommandHandler, shellController,
+                displayController, pipAnimationController, pipAppOpsListener,
+                pipBoundsAlgorithm,
+                pipKeepClearAlgorithm, pipBoundsState, pipDisplayLayoutState,
+                pipMotionHelper, pipMediaController, phonePipMenuController, pipTaskOrganizer,
+                pipTransitionState, pipTouchHandler, pipTransitionController,
+                windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+                displayInsetsController, pipTabletopController, oneHandedController,
+                mainExecutor));
     }
 
     // Handler is used by Icon.loadDrawableAsync
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 6968317..ea7e968 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -81,6 +81,16 @@
 
     @WMSingleton
     @Provides
+    static Optional<PipController.PipImpl> providePip2(Optional<PipController> pipController) {
+        if (pipController.isEmpty()) {
+            return Optional.empty();
+        } else {
+            return Optional.ofNullable(pipController.get().getPipImpl());
+        }
+    }
+
+    @WMSingleton
+    @Provides
     static Optional<PipController> providePipController(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
index f2631ef..a3afe78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/PipModule.java
@@ -18,12 +18,16 @@
 
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipTransition;
 
 import dagger.Module;
 import dagger.Provides;
 
+import java.util.Optional;
+
 /**
  * Provides dependencies for external components / modules reference PiP and extracts away the
  * selection of legacy and new PiP implementation.
@@ -44,4 +48,17 @@
             return legacyPipTransition;
         }
     }
+
+    @WMSingleton
+    @Provides
+    static Optional<Pip> providePip(
+            Optional<com.android.wm.shell.pip.phone.PipController.PipImpl> pip1,
+            Optional<PipController.PipImpl> pip2) {
+        if (PipUtils.isPip2ExperimentEnabled()) {
+            return Optional.ofNullable(pip2.orElse(null));
+
+        } else {
+            return Optional.ofNullable(pip1.orElse(null));
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index dc3e2d0..5807246 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -217,6 +217,15 @@
         dragToDesktopTransitionHandler.setSplitScreenController(controller)
     }
 
+    /** Returns the transition type for the given remote transition. */
+    private fun transitionType(remoteTransition: RemoteTransition?): Int {
+        if (remoteTransition == null) {
+            ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: remoteTransition is null")
+            return TRANSIT_NONE
+        }
+        return TRANSIT_TO_FRONT
+    }
+
     /** Show all tasks, that are part of the desktop, on top of launcher */
     fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) {
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
@@ -224,8 +233,7 @@
         bringDesktopAppsToFront(displayId, wct)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            // TODO(b/309014605): ensure remote transition is supplied once state is introduced
-            val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT
+            val transitionType = transitionType(remoteTransition)
             val handler =
                 remoteTransition?.let {
                     OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
@@ -764,15 +772,13 @@
             newTaskIdInFront ?: "null"
         )
 
+        // Move home to front, ensures that we go back home when all desktop windows are closed
+        moveHomeTask(wct, toTop = true)
+
         // Currently, we only handle the desktop on the default display really.
-        if (displayId == DEFAULT_DISPLAY) {
-            if (Flags.enableDesktopWindowingWallpaperActivity()) {
-                // Add translucent wallpaper activity to show the wallpaper underneath
-                addWallpaperActivity(wct)
-            } else {
-                // Move home to front
-                moveHomeTask(wct, toTop = true)
-            }
+        if (displayId == DEFAULT_DISPLAY && Flags.enableDesktopWindowingWallpaperActivity()) {
+            // Add translucent wallpaper activity to show the wallpaper underneath
+            addWallpaperActivity(wct)
         }
 
         val nonMinimizedTasksOrderedFrontToBack =
@@ -1101,6 +1107,10 @@
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
         }
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+            // Remove wallpaper activity when leaving desktop mode
+            removeWallpaperActivity(wct)
+        }
     }
 
     /**
@@ -1116,6 +1126,10 @@
         // The task's density may have been overridden in freeform; revert it here as we don't
         // want it overridden in multi-window.
         wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
+        if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskInfo.taskId)) {
+            // Remove wallpaper activity when leaving desktop mode
+            removeWallpaperActivity(wct)
+        }
     }
 
     /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 3572d16..84f6af41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -68,7 +68,7 @@
 ## Tracing global SurfaceControl transaction updates
 
 While Winscope traces are very useful, it sometimes doesn't give you enough information about which
-part of the code is initiating the transaction updates.  In such cases, it can be helpful to get
+part of the code is initiating the transaction updates. In such cases, it can be helpful to get
 stack traces when specific surface transaction calls are made, which is possible by enabling the
 following system properties for example:
 ```shell
@@ -81,9 +81,11 @@
 # Disabling logging
 adb shell setprop persist.wm.debug.sc.tx.log_match_call \"\"
 adb shell setprop persist.wm.debug.sc.tx.log_match_name \"\"
-adb reboot
 ```
 
+A reboot is required to enable the logging. Once enabled, reboot is not needed to update the
+properties.
+
 It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
 noisy if unfiltered.
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index d1d8275..0cb7e17 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -374,7 +374,7 @@
      * Instantiates {@link PipController}, returns {@code null} if the feature not supported.
      */
     @Nullable
-    public static Pip create(Context context,
+    public static PipImpl create(Context context,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
             ShellController shellController,
@@ -1177,7 +1177,7 @@
     /**
      * The interface for calls from outside the Shell, within the host process.
      */
-    private class PipImpl implements Pip {
+    public class PipImpl implements Pip {
         @Override
         public void expandPip() {
             mMainExecutor.execute(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 06adad6..c12219c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -55,6 +55,8 @@
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
 import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -62,6 +64,8 @@
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Manages the picture-in-picture (PIP) UI and states for Phones.
@@ -86,6 +90,8 @@
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final PipTransitionState mPipTransitionState;
     private final ShellExecutor mMainExecutor;
+    private final PipImpl mImpl;
+    private Consumer<Boolean> mOnIsInPipStateChangedListener;
 
     // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents.
     private PipAnimationListener mPipRecentsAnimationListener;
@@ -140,6 +146,7 @@
         mPipTransitionState = pipTransitionState;
         mPipTransitionState.addPipTransitionStateChangedListener(this);
         mMainExecutor = mainExecutor;
+        mImpl = new PipImpl();
 
         if (PipUtils.isPip2ExperimentEnabled()) {
             shellInit.addInitCallback(this::onInit, this);
@@ -174,6 +181,10 @@
                 pipTransitionState, mainExecutor);
     }
 
+    public PipImpl getPipImpl() {
+        return mImpl;
+    }
+
     private void onInit() {
         mShellCommandHandler.addDumpCallback(this::dump, this);
         // Ensure that we have the display info in case we get calls to update the bounds before the
@@ -310,22 +321,29 @@
     @Override
     public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
             @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) {
-        if (newState == PipTransitionState.SWIPING_TO_PIP) {
-            Preconditions.checkState(extra != null,
-                    "No extra bundle for " + mPipTransitionState);
+        switch (newState) {
+            case PipTransitionState.SWIPING_TO_PIP:
+                Preconditions.checkState(extra != null,
+                        "No extra bundle for " + mPipTransitionState);
 
-            SurfaceControl overlay = extra.getParcelable(
-                    SWIPE_TO_PIP_OVERLAY, SurfaceControl.class);
-            Rect appBounds = extra.getParcelable(
-                    SWIPE_TO_PIP_APP_BOUNDS, Rect.class);
+                SurfaceControl overlay = extra.getParcelable(
+                        SWIPE_TO_PIP_OVERLAY, SurfaceControl.class);
+                Rect appBounds = extra.getParcelable(
+                        SWIPE_TO_PIP_APP_BOUNDS, Rect.class);
 
-            Preconditions.checkState(appBounds != null,
-                    "App bounds can't be null for " + mPipTransitionState);
-            mPipTransitionState.setSwipePipToHomeState(overlay, appBounds);
-        } else if (newState == PipTransitionState.ENTERED_PIP) {
-            if (mPipTransitionState.isInSwipePipToHomeTransition()) {
-                mPipTransitionState.resetSwipePipToHomeState();
-            }
+                Preconditions.checkState(appBounds != null,
+                        "App bounds can't be null for " + mPipTransitionState);
+                mPipTransitionState.setSwipePipToHomeState(overlay, appBounds);
+                break;
+            case PipTransitionState.ENTERED_PIP:
+                if (mPipTransitionState.isInSwipePipToHomeTransition()) {
+                    mPipTransitionState.resetSwipePipToHomeState();
+                }
+                mOnIsInPipStateChangedListener.accept(true /* inPip */);
+                break;
+            case PipTransitionState.EXITED_PIP:
+                mOnIsInPipStateChangedListener.accept(false /* inPip */);
+                break;
         }
     }
 
@@ -355,6 +373,53 @@
         mPipTransitionState.dump(pw, innerPrefix);
     }
 
+    private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+        mOnIsInPipStateChangedListener = callback;
+        if (mOnIsInPipStateChangedListener != null) {
+            callback.accept(mPipTransitionState.isInPip());
+        }
+    }
+
+    /**
+     * The interface for calls from outside the Shell, within the host process.
+     */
+    public class PipImpl implements Pip {
+        @Override
+        public void expandPip() {}
+
+        @Override
+        public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {}
+
+        @Override
+        public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+            mMainExecutor.execute(() -> {
+                PipController.this.setOnIsInPipStateChangedListener(callback);
+            });
+        }
+
+        @Override
+        public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
+            mMainExecutor.execute(() -> {
+                mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
+            });
+        }
+
+        @Override
+        public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) {
+            mMainExecutor.execute(() -> {
+                mPipBoundsState.removePipExclusionBoundsChangeCallback(listener);
+            });
+        }
+
+        @Override
+        public void showPictureInPictureMenu() {}
+
+        @Override
+        public void registerPipTransitionCallback(
+                PipTransitionController.PipTransitionCallback callback,
+                Executor executor) {}
+    }
+
     /**
      * The interface for calls from outside the host process.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index f33a573..7784784 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -41,6 +41,7 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -63,6 +64,7 @@
 import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
+import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange;
 import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
 
 import android.animation.Animator;
@@ -353,6 +355,7 @@
                 continue;
             }
             final boolean isTask = change.getTaskInfo() != null;
+            final boolean isFreeform = isTask && change.getTaskInfo().isFreeform();
             final int mode = change.getMode();
             boolean isSeamlessDisplayChange = false;
 
@@ -459,6 +462,16 @@
                             final int layer = zSplitLine + numChanges - i;
                             startTransaction.setLayer(change.getLeash(), layer);
                         }
+                    } else if (!isCoveredByOpaqueFullscreenChange(info, change)
+                            && isFreeform
+                            && TransitionUtil.isOpeningMode(type)
+                            && change.getMode() == TRANSIT_TO_BACK) {
+                        // Reparent the minimize-change to the root task so the minimizing Task
+                        // isn't shown in front of other Tasks.
+                        mRootTDAOrganizer.reparentToDisplayArea(
+                                change.getTaskInfo().displayId,
+                                change.getLeash(),
+                                startTransaction);
                     } else if (isOnlyTranslucent && TransitionUtil.isOpeningType(info.getType())
                                 && TransitionUtil.isClosingType(mode)) {
                         // If there is a closing translucent task in an OPENING transition, we will
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index a5f071a..75e7ddf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -36,6 +36,7 @@
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.WindowConfiguration;
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -72,6 +73,9 @@
         final int changeFlags = change.getFlags();
         final boolean enter = TransitionUtil.isOpeningType(changeMode);
         final boolean isTask = change.getTaskInfo() != null;
+        final boolean isFreeform = isTask && change.getTaskInfo().isFreeform();
+        final boolean isCoveredByOpaqueFullscreenChange =
+                isCoveredByOpaqueFullscreenChange(info, change);
         final TransitionInfo.AnimationOptions options;
         if (Flags.moveAnimationOptionsToChange()) {
             options = change.getAnimationOptions();
@@ -107,6 +111,24 @@
             animAttr = enter
                     ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation
                     : R.styleable.WindowAnimation_wallpaperCloseExitAnimation;
+        } else if (!isCoveredByOpaqueFullscreenChange
+                && isFreeform
+                && TransitionUtil.isOpeningMode(type)
+                && change.getMode() == TRANSIT_TO_BACK) {
+            // Set translucent here so TransitionAnimation loads the appropriate animations for
+            // translucent activities and tasks later
+            translucent = (changeFlags & FLAG_TRANSLUCENT) != 0;
+            // The main Task is launching or being brought to front, this Task is being minimized
+            animAttr = R.styleable.WindowAnimation_activityCloseExitAnimation;
+        } else if (!isCoveredByOpaqueFullscreenChange
+                && isFreeform
+                && type == TRANSIT_TO_FRONT
+                && change.getMode() == TRANSIT_TO_FRONT) {
+            // Set translucent here so TransitionAnimation loads the appropriate animations for
+            // translucent activities and tasks later
+            translucent = (changeFlags & FLAG_TRANSLUCENT) != 0;
+            // Bring the minimized Task back to front
+            animAttr = R.styleable.WindowAnimation_activityOpenEnterAnimation;
         } else if (type == TRANSIT_OPEN) {
             // We will translucent open animation for translucent activities and tasks. Choose
             // WindowAnimation_activityOpenEnterAnimation and set translucent here, then
@@ -417,4 +439,25 @@
 
         return edgeExtensionLayer;
     }
+
+    /**
+     * Returns whether there is an opaque fullscreen Change positioned in front of the given Change
+     * in the given TransitionInfo.
+     */
+    static boolean isCoveredByOpaqueFullscreenChange(
+            TransitionInfo info, TransitionInfo.Change change) {
+        // TransitionInfo#getChanges() are ordered from front to back
+        for (TransitionInfo.Change coveringChange : info.getChanges()) {
+            if (coveringChange == change) {
+                return false;
+            }
+            if ((coveringChange.getFlags() & FLAG_TRANSLUCENT) == 0
+                    && coveringChange.getTaskInfo() != null
+                    && coveringChange.getTaskInfo().getWindowingMode()
+                    == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 95e0d79..b9cb6d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,12 +26,20 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
+import android.hardware.input.InputManager;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.provider.Settings;
+import android.util.Log;
 import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.Display;
+import android.view.ISystemGestureExclusionListener;
+import android.view.IWindowManager;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -45,39 +53,74 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 
 /**
  * View model for the window decoration with a caption and shadows. Works with
  * {@link CaptionWindowDecoration}.
  */
 public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
+    private static final String TAG = "CaptionWindowDecorViewModel";
+
     private final ShellTaskOrganizer mTaskOrganizer;
+    private final IWindowManager mWindowManager;
     private final Context mContext;
     private final Handler mMainHandler;
+    private final ShellExecutor mMainExecutor;
     private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private final SyncTransactionQueue mSyncQueue;
     private final Transitions mTransitions;
+    private final Region mExclusionRegion = Region.obtain();
+    private final InputManager mInputManager;
     private TaskOperations mTaskOperations;
 
+    /**
+     * Whether to pilfer the next motion event to send cancellations to the windows below.
+     * Useful when the caption window is spy and the gesture should be handled by the system
+     * instead of by the app for their custom header content.
+     */
+    private boolean mShouldPilferCaptionEvents;
+
     private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
 
+    private final ISystemGestureExclusionListener mGestureExclusionListener =
+            new ISystemGestureExclusionListener.Stub() {
+                @Override
+                public void onSystemGestureExclusionChanged(int displayId,
+                        Region systemGestureExclusion, Region systemGestureExclusionUnrestricted) {
+                    if (mContext.getDisplayId() != displayId) {
+                        return;
+                    }
+                    mMainExecutor.execute(() -> {
+                        mExclusionRegion.set(systemGestureExclusion);
+                    });
+                }
+            };
+
     public CaptionWindowDecorViewModel(
             Context context,
             Handler mainHandler,
+            ShellExecutor shellExecutor,
             Choreographer mainChoreographer,
+            IWindowManager windowManager,
+            ShellInit shellInit,
             ShellTaskOrganizer taskOrganizer,
             DisplayController displayController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             SyncTransactionQueue syncQueue,
             Transitions transitions) {
         mContext = context;
+        mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
+        mWindowManager = windowManager;
         mMainChoreographer = mainChoreographer;
         mTaskOrganizer = taskOrganizer;
         mDisplayController = displayController;
@@ -87,6 +130,18 @@
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
             mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
         }
+        mInputManager = mContext.getSystemService(InputManager.class);
+
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        try {
+            mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
+                    mContext.getDisplayId());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to register window manager callbacks", e);
+        }
     }
 
     @Override
@@ -178,8 +233,12 @@
     }
 
     private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) {
-        final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
-        decoration.setCaptionColor(statusBarColor);
+        if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
+            decoration.setCaptionColor(Color.TRANSPARENT);
+        } else {
+            final int statusBarColor = taskInfo.taskDescription.getStatusBarColor();
+            decoration.setCaptionColor(statusBarColor);
+        }
     }
 
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
@@ -301,6 +360,49 @@
                     mSyncQueue.queue(wct);
                 }
             }
+            final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+
+            final int actionMasked = e.getActionMasked();
+            final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN;
+            final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL
+                    || actionMasked == MotionEvent.ACTION_UP;
+            if (isDown) {
+                final boolean downInCustomizableCaptionRegion =
+                        decoration.checkTouchEventInCustomizableRegion(e);
+                final boolean downInExclusionRegion = mExclusionRegion.contains(
+                        (int) e.getRawX(), (int) e.getRawY());
+                final boolean isTransparentCaption =
+                        TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo);
+                // MotionEvent's coordinates are relative to view, we want location in window
+                // to offset position relative to caption as a whole.
+                int[] viewLocation = new int[2];
+                v.getLocationInWindow(viewLocation);
+                final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e,
+                        new Point(viewLocation[0], viewLocation[1]));
+                // The caption window may be a spy window when the caption background is
+                // transparent, which means events will fall through to the app window. Make
+                // sure to cancel these events if they do not happen in the intersection of the
+                // customizable region and what the app reported as exclusion areas, because
+                // the drag-move or other caption gestures should take priority outside those
+                // regions.
+                mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion
+                        && downInExclusionRegion && isTransparentCaption) && !isResizeEvent;
+            }
+
+            if (!mShouldPilferCaptionEvents) {
+                // The event will be handled by a window below or pilfered by resize handler.
+                return false;
+            }
+            // Otherwise pilfer so that windows below receive cancellations for this gesture, and
+            // continue normal handling as a caption gesture.
+            if (mInputManager != null) {
+                // TODO(b/352127475): Only pilfer once per gesture
+                mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
+            }
+            if (isUpOrCancel) {
+                // Gesture is finished, reset state.
+                mShouldPilferCaptionEvents = false;
+            }
             return mDragDetector.onMotionEvent(e);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index d0ca5b0..7e1b973 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -21,6 +21,7 @@
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
 
 import android.annotation.NonNull;
+import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
 import android.app.WindowConfiguration.WindowingMode;
@@ -28,22 +29,27 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
 import android.util.Size;
 import android.view.Choreographer;
+import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.WindowManager;
 import android.window.WindowContainerTransaction;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 
 /**
  * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -177,12 +183,44 @@
                 shouldSetTaskPositionAndCrop);
     }
 
+    @VisibleForTesting
+    static void updateRelayoutParams(
+            RelayoutParams relayoutParams,
+            ActivityManager.RunningTaskInfo taskInfo,
+            boolean applyStartTransactionOnDraw,
+            boolean setTaskCropAndPosition) {
+        relayoutParams.reset();
+        relayoutParams.mRunningTaskInfo = taskInfo;
+        relayoutParams.mLayoutResId = R.layout.caption_window_decor;
+        relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
+        relayoutParams.mShadowRadiusId = taskInfo.isFocused
+                ? R.dimen.freeform_decor_shadow_focused_thickness
+                : R.dimen.freeform_decor_shadow_unfocused_thickness;
+        relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
+        relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
+
+        if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
+            // If the app is requesting to customize the caption bar, allow input to fall
+            // through to the windows below so that the app can respond to input events on
+            // their custom content.
+            relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+        }
+        final RelayoutParams.OccludingCaptionElement backButtonElement =
+                new RelayoutParams.OccludingCaptionElement();
+        backButtonElement.mWidthResId = R.dimen.caption_left_buttons_width;
+        backButtonElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START;
+        relayoutParams.mOccludingCaptionElements.add(backButtonElement);
+        // Then, the right-aligned section (minimize, maximize and close buttons).
+        final RelayoutParams.OccludingCaptionElement controlsElement =
+                new RelayoutParams.OccludingCaptionElement();
+        controlsElement.mWidthResId = R.dimen.caption_right_buttons_width;
+        controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END;
+        relayoutParams.mOccludingCaptionElements.add(controlsElement);
+    }
+
     void relayout(RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
-        final int shadowRadiusID = taskInfo.isFocused
-                ? R.dimen.freeform_decor_shadow_focused_thickness
-                : R.dimen.freeform_decor_shadow_unfocused_thickness;
         final boolean isFreeform =
                 taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
         final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
@@ -191,13 +229,8 @@
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
 
-        mRelayoutParams.reset();
-        mRelayoutParams.mRunningTaskInfo = taskInfo;
-        mRelayoutParams.mLayoutResId = R.layout.caption_window_decor;
-        mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode());
-        mRelayoutParams.mShadowRadiusId = shadowRadiusID;
-        mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
-        mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
+        updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
+                setTaskCropAndPosition);
 
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
@@ -303,6 +336,17 @@
         mDragResizeListener = null;
     }
 
+    /**
+     * Checks whether the touch event falls inside the customizable caption region.
+     */
+    boolean checkTouchEventInCustomizableRegion(MotionEvent ev) {
+        return mResult.mCustomizableCaptionRegion.contains((int) ev.getRawX(), (int) ev.getRawY());
+    }
+
+    boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
+        return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset);
+    }
+
     @Override
     public void close() {
         closeDragResizeListener();
@@ -311,6 +355,10 @@
 
     @Override
     int getCaptionHeightId(@WindowingMode int windowingMode) {
+        return getCaptionHeightIdStatic(windowingMode);
+    }
+
+    private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) {
         return R.dimen.freeform_decor_caption_height;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index faf6a62..d12b906 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -1040,7 +1040,7 @@
     private void createInputChannel(int displayId) {
         final InputManager inputManager = mContext.getSystemService(InputManager.class);
         final InputMonitor inputMonitor =
-                mInputMonitorFactory.create(inputManager, mContext);
+                mInputMonitorFactory.create(inputManager, displayId);
         final EventReceiver eventReceiver = new EventReceiver(inputMonitor,
                 inputMonitor.getInputChannel(), Looper.myLooper());
         mEventReceiversByDisplay.put(displayId, eventReceiver);
@@ -1194,8 +1194,8 @@
     }
 
     static class InputMonitorFactory {
-        InputMonitor create(InputManager inputManager, Context context) {
-            return inputManager.monitorGestureInput("caption-touch", context.getDisplayId());
+        InputMonitor create(InputManager inputManager, int displayId) {
+            return inputManager.monitorGestureInput("caption-touch", displayId);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 39595cf..bce233f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.windowdecor
 
+import android.annotation.DimenRes
 import android.app.ActivityManager
 import android.app.WindowConfiguration
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -75,10 +76,17 @@
     private val isViewAboveStatusBar: Boolean
         get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform)
 
-    private var marginMenuTop = 0
-    private var marginMenuStart = 0
-    private var menuHeight = 0
-    private var menuWidth = 0
+    private val pillElevation: Int = loadDimensionPixelSize(
+        R.dimen.desktop_mode_handle_menu_pill_elevation)
+    private val pillTopMargin: Int = loadDimensionPixelSize(
+        R.dimen.desktop_mode_handle_menu_pill_spacing_margin)
+    private val menuWidth = loadDimensionPixelSize(
+        R.dimen.desktop_mode_handle_menu_width) + pillElevation
+    private val menuHeight = getHandleMenuHeight()
+    private val marginMenuTop = loadDimensionPixelSize(R.dimen.desktop_mode_handle_menu_margin_top)
+    private val marginMenuStart = loadDimensionPixelSize(
+        R.dimen.desktop_mode_handle_menu_margin_start)
+
     private var handleMenuAnimator: HandleMenuAnimator? = null
 
     @VisibleForTesting
@@ -120,7 +128,6 @@
         }
 
     init {
-        loadHandleMenuDimensions()
         updateHandleMenuPillPositions()
     }
 
@@ -426,49 +433,35 @@
      */
     private fun viewsLaidOut(): Boolean = handleMenuViewContainer?.view?.isLaidOut ?: false
 
-    private fun loadHandleMenuDimensions() {
-        val resources = context.resources
-        menuWidth = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_width)
-        menuHeight = getHandleMenuHeight(resources)
-        marginMenuTop = loadDimensionPixelSize(
-            resources,
-            R.dimen.desktop_mode_handle_menu_margin_top
-        )
-        marginMenuStart = loadDimensionPixelSize(
-            resources,
-            R.dimen.desktop_mode_handle_menu_margin_start
-        )
-    }
-
     /**
-     * Determines handle menu height based on if windowing pill should be shown.
+     * Determines handle menu height based the max size and the visibility of pills.
      */
-    private fun getHandleMenuHeight(resources: Resources): Int {
-        var menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height)
+    private fun getHandleMenuHeight(): Int {
+        var menuHeight = loadDimensionPixelSize(
+            R.dimen.desktop_mode_handle_menu_height) + pillElevation
         if (!shouldShowWindowingPill) {
             menuHeight -= loadDimensionPixelSize(
-                resources,
-                R.dimen.desktop_mode_handle_menu_windowing_pill_height
-            )
+                R.dimen.desktop_mode_handle_menu_windowing_pill_height)
+            menuHeight -= pillTopMargin
         }
         if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
             menuHeight -= loadDimensionPixelSize(
-                resources,
-                R.dimen.desktop_mode_handle_menu_more_actions_pill_height
-            )
+                R.dimen.desktop_mode_handle_menu_more_actions_pill_height)
+            menuHeight -= pillTopMargin
         }
         if (!shouldShowBrowserPill) {
-            menuHeight -= loadDimensionPixelSize(resources,
+            menuHeight -= loadDimensionPixelSize(
                 R.dimen.desktop_mode_handle_menu_open_in_browser_pill_height)
+            menuHeight -= pillTopMargin
         }
         return menuHeight
     }
 
-    private fun loadDimensionPixelSize(resources: Resources, resourceId: Int): Int {
+    private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
         if (resourceId == Resources.ID_NULL) {
             return 0
         }
-        return resources.getDimensionPixelSize(resourceId)
+        return context.resources.getDimensionPixelSize(resourceId)
     }
 
     fun close() {
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt
new file mode 100644
index 0000000..0b6c9af
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+
+@Ignore("Base Test Class")
+abstract class ExitDesktopWithDragToTopDragZone
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+
+    @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        tapl.setEnableRotation(true)
+        tapl.setExpectedRotation(rotation.value)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+    }
+
+    @Test
+    open fun exitDesktopWithDragToTopDragZone() {
+        testApp.exitDesktopWithDragToTopDragZone(wmHelper, device)
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 4d1b6ba..8558a77 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -313,6 +313,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
+    val homeTask = setUpHomeTask(SECOND_DISPLAY)
     val task1 = setUpFreeformTask(SECOND_DISPLAY)
     val task2 = setUpFreeformTask(SECOND_DISPLAY)
     markTaskHidden(task1)
@@ -321,10 +322,11 @@
     controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    // Expect order to be from bottom: task1, task2 (no wallpaper intent)
-    wct.assertReorderAt(index = 0, task1)
-    wct.assertReorderAt(index = 1, task2)
+    assertThat(wct.hierarchyOps).hasSize(3)
+    // Expect order to be from bottom: home, task1, task2 (no wallpaper intent)
+    wct.assertReorderAt(index = 0, homeTask)
+    wct.assertReorderAt(index = 1, task1)
+    wct.assertReorderAt(index = 2, task2)
   }
 
   @Test
@@ -349,6 +351,7 @@
   @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() {
+    val homeTask = setUpHomeTask(SECOND_DISPLAY)
     val task1 = setUpFreeformTask(SECOND_DISPLAY)
     val task2 = setUpFreeformTask(SECOND_DISPLAY)
     markTaskHidden(task1)
@@ -357,9 +360,11 @@
     controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    wct.assertReorderAt(index = 0, task1)
-    wct.assertReorderAt(index = 1, task2)
+    assertThat(wct.hierarchyOps).hasSize(3)
+    // Expect order to be from bottom: home, task1, task2
+    wct.assertReorderAt(index = 0, homeTask)
+    wct.assertReorderAt(index = 1, task1)
+    wct.assertReorderAt(index = 2, task2)
   }
 
   @Test
@@ -460,6 +465,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
+    val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
     val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
     setUpHomeTask(SECOND_DISPLAY)
     val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
@@ -469,10 +475,13 @@
     controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
-    // Expect order to be from bottom: wallpaper intent, task
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
-    wct.assertReorderAt(index = 1, taskDefaultDisplay)
+    assertThat(wct.hierarchyOps).hasSize(3)
+    // Move home to front
+    wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
+    // Add desktop wallpaper activity
+    wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
+    // Move freeform task to front
+    wct.assertReorderAt(index = 2, taskDefaultDisplay)
   }
 
   @Test
@@ -497,7 +506,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
-    setUpHomeTask()
+    val homeTask = setUpHomeTask()
     val freeformTask = setUpFreeformTask()
     val minimizedTask = setUpFreeformTask()
 
@@ -507,11 +516,13 @@
     controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
     val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
-    assertThat(wct.hierarchyOps).hasSize(2)
+    assertThat(wct.hierarchyOps).hasSize(3)
+    // Move home to front
+    wct.assertReorderAt(index = 0, homeTask, toTop = true)
     // Add desktop wallpaper activity
-    wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+    wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
     // Reorder freeform task to top, don't reorder the minimized task
-    wct.assertReorderAt(index = 1, freeformTask, toTop = true)
+    wct.assertReorderAt(index = 2, freeformTask, toTop = true)
   }
 
   @Test
@@ -894,16 +905,19 @@
     val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
     val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
     val newTask = setUpFullscreenTask()
-    setUpHomeTask()
+    val homeTask = setUpHomeTask()
 
     controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
 
     val wct = getLatestEnterDesktopWct()
-    assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + wallpaper
+    assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 2) // tasks + home + wallpaper
+    // Move home to front
+    wct.assertReorderAt(0, homeTask)
     // Add desktop wallpaper activity
-    wct.assertPendingIntentAt(0, desktopWallpaperIntent)
+    wct.assertPendingIntentAt(1, desktopWallpaperIntent)
+    // Bring freeform tasks to front
     wct.assertReorderSequenceInRange(
-      range = 1..<(taskLimit + 1),
+      range = 2..<(taskLimit + 2),
       *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
       newTask
     )
@@ -921,6 +935,24 @@
   }
 
   @Test
+  fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    // Removes wallpaper activity when leaving desktop
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
   fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
     val task = setUpFreeformTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -932,6 +964,44 @@
   }
 
   @Test
+  fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
+    val task = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+    controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN)
+    // Removes wallpaper activity when leaving desktop
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    // Setup task2
+    setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+      .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+    controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
+    assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
+    // Does not remove wallpaper activity, as desktop still has a visible desktop task
+    assertThat(wct.hierarchyOps).isEmpty()
+  }
+
+  @Test
   fun moveToFullscreen_nonExistentTask_doesNothing() {
     controller.moveToFullscreen(999, transitionSource = UNKNOWN)
     verifyExitDesktopWCTNotExecuted()
@@ -1755,6 +1825,49 @@
   }
 
   @Test
+  fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+      visible = false)
+
+    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    wct.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+    val wct = getLatestExitDesktopWct()
+    val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
+    assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+    assertThat(wct.hierarchyOps).isEmpty()
+  }
+
+  @Test
   @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
   fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
     val spyController = spy(controller)
@@ -1963,6 +2076,7 @@
             eq(null))
   }
 
+  @Test
   fun enterSplit_freeformTaskIsMovedToSplit() {
     val task1 = setUpFreeformTask()
     val task2 = setUpFreeformTask()
@@ -1972,14 +2086,67 @@
     task2.isFocused = true
     task3.isFocused = false
 
-    controller.enterSplit(DEFAULT_DISPLAY, false)
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
 
     verify(splitScreenController)
         .requestEnterSplitSelect(
-            task2,
+            eq(task2),
             any(),
-            SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
-            task2.configuration.windowConfiguration.bounds)
+            eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+            eq(task2.configuration.windowConfiguration.bounds))
+  }
+
+  @Test
+  fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+    desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId)
+    desktopModeTaskRepository.updateVisibleFreeformTasks(DEFAULT_DISPLAY, task3.taskId,
+      visible = false)
+
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    verify(splitScreenController)
+      .requestEnterSplitSelect(
+        eq(task2),
+        wctArgument.capture(),
+        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+        eq(task2.configuration.windowConfiguration.bounds))
+    // Removes wallpaper activity when leaving desktop
+    wctArgument.value.assertRemoveAt(index = 0, wallpaperToken)
+  }
+
+  @Test
+  fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
+    val task1 = setUpFreeformTask()
+    val task2 = setUpFreeformTask()
+    val task3 = setUpFreeformTask()
+    val wallpaperToken = MockToken().token()
+
+    task1.isFocused = false
+    task2.isFocused = true
+    task3.isFocused = false
+    desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+
+    controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
+
+    val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    verify(splitScreenController)
+      .requestEnterSplitSelect(
+        eq(task2),
+        wctArgument.capture(),
+        eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
+        eq(task2.configuration.windowConfiguration.bounds))
+    // Does not remove wallpaper activity, as desktop still has visible desktop tasks
+    assertThat(wctArgument.value.hierarchyOps).isEmpty()
   }
 
   @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
index 754a173..6bc7e49 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.transition;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_SLEEP;
@@ -185,6 +186,73 @@
         verify(startT, never()).setColor(any(), any());
     }
 
+    @Test
+    public void startAnimation_freeformOpenChange_doesntReparentTask() {
+        final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN)
+                .setTask(createTaskInfo(
+                        /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN))
+                .build();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openChange)
+                .build();
+        final IBinder token = new Binder();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        verify(startT, never()).reparent(any(), any());
+    }
+
+    @Test
+    public void startAnimation_freeformMinimizeChange_underFullscreenChange_doesntReparentTask() {
+        final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN)
+                .setTask(createTaskInfo(
+                        /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FULLSCREEN))
+                .build();
+        final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK)
+                .setTask(createTaskInfo(
+                        /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM))
+                .build();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openChange)
+                .addChange(toBackChange)
+                .build();
+        final IBinder token = new Binder();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        verify(startT, never()).reparent(any(), any());
+    }
+
+    @Test
+    public void startAnimation_freeform_minimizeAnimation_reparentsTask() {
+        final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN)
+                .setTask(createTaskInfo(
+                        /* taskId= */ 1, /* windowingMode= */ WINDOWING_MODE_FREEFORM))
+                .build();
+        final TransitionInfo.Change toBackChange = new ChangeBuilder(TRANSIT_TO_BACK)
+                .setTask(createTaskInfo(
+                        /* taskId= */ 2, /* windowingMode= */ WINDOWING_MODE_FREEFORM))
+                .build();
+        final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(openChange)
+                .addChange(toBackChange)
+                .build();
+        final IBinder token = new Binder();
+        final SurfaceControl.Transaction startT = MockTransactionPool.create();
+        final SurfaceControl.Transaction finishT = MockTransactionPool.create();
+
+        mTransitionHandler.startAnimation(token, info, startT, finishT,
+                mock(Transitions.TransitionFinishCallback.class));
+
+        verify(startT).reparent(any(), any());
+    }
+
     private static void mergeSync(Transitions.TransitionHandler handler, IBinder token) {
         handler.mergeAnimation(
                 new Binder(),
@@ -195,10 +263,14 @@
     }
 
     private static RunningTaskInfo createTaskInfo(int taskId) {
+        return createTaskInfo(taskId, WINDOWING_MODE_FULLSCREEN);
+    }
+
+    private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.topActivityType = ACTIVITY_TYPE_STANDARD;
-        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
         taskInfo.configuration.windowConfiguration.setActivityType(taskInfo.topActivityType);
         taskInfo.token = mock(WindowContainerToken.class);
         return taskInfo;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt
new file mode 100644
index 0000000..bad14bb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionAnimationHelperTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.transition
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionInfo.FLAG_TRANSLUCENT
+import com.android.internal.R
+import com.android.internal.policy.TransitionAnimation
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+
+class TransitionAnimationHelperTest : ShellTestCase() {
+
+    @Mock
+    lateinit var transitionAnimation: TransitionAnimation
+
+    @Test
+    fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_returnsMinimizeAnim() {
+        val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM))
+            .build()
+        val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM))
+            .build()
+        val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN)
+            .addChange(openChange)
+            .addChange(toBackChange)
+            .build()
+
+        loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange)
+
+        verify(transitionAnimation).loadDefaultAnimationAttr(
+            eq(R.styleable.WindowAnimation_activityCloseExitAnimation), anyBoolean())
+    }
+
+    @Test
+    fun loadAttributeAnimation_freeform_taskToFront_taskToFrontChange_returnsUnminimizeAnim() {
+        val toFrontChange = ChangeBuilder(WindowManager.TRANSIT_TO_FRONT)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM))
+            .build()
+        val info = TransitionInfoBuilder(WindowManager.TRANSIT_TO_FRONT)
+            .addChange(toFrontChange)
+            .build()
+
+        loadAttributeAnimation(WindowManager.TRANSIT_TO_FRONT, info, toFrontChange)
+
+        verify(transitionAnimation).loadDefaultAnimationAttr(
+            eq(R.styleable.WindowAnimation_activityOpenEnterAnimation),
+            /* translucent= */ anyBoolean())
+    }
+
+    @Test
+    fun loadAttributeAnimation_fullscreen_taskOpen_returnsTaskOpenEnterAnim() {
+        val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN))
+            .build()
+        val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN).addChange(openChange).build()
+
+        loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, openChange)
+
+        verify(transitionAnimation).loadDefaultAnimationAttr(
+            eq(R.styleable.WindowAnimation_taskOpenEnterAnimation),
+            /* translucent= */ anyBoolean())
+    }
+
+    @Test
+    fun loadAttributeAnimation_freeform_taskOpen_taskToBackChange_passesTranslucent() {
+        val openChange = ChangeBuilder(WindowManager.TRANSIT_OPEN)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM))
+            .build()
+        val toBackChange = ChangeBuilder(WindowManager.TRANSIT_TO_BACK)
+            .setTask(createTaskInfo(WindowConfiguration.WINDOWING_MODE_FREEFORM))
+            .setFlags(FLAG_TRANSLUCENT)
+            .build()
+        val info = TransitionInfoBuilder(WindowManager.TRANSIT_OPEN)
+            .addChange(openChange)
+            .addChange(toBackChange)
+            .build()
+
+        loadAttributeAnimation(WindowManager.TRANSIT_OPEN, info, toBackChange)
+
+        verify(transitionAnimation).loadDefaultAnimationAttr(
+            eq(R.styleable.WindowAnimation_activityCloseExitAnimation),
+            /* translucent= */ eq(true))
+    }
+
+    private fun loadAttributeAnimation(
+        @WindowManager.TransitionType type: Int,
+        info: TransitionInfo,
+        change: TransitionInfo.Change,
+        wallpaperTransit: Int = TransitionAnimation.WALLPAPER_TRANSITION_NONE,
+        isDreamTransition: Boolean = false,
+    ) {
+        TransitionAnimationHelper.loadAttributeAnimation(
+            type, info, change, wallpaperTransit, transitionAnimation, isDreamTransition)
+    }
+
+    private fun createTaskInfo(windowingMode: Int): RunningTaskInfo {
+        val taskInfo = TestRunningTaskInfoBuilder()
+            .setWindowingMode(windowingMode)
+            .build()
+        return taskInfo
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
new file mode 100644
index 0000000..261d4b5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.windowdecor
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import android.view.Display
+import android.view.WindowInsetsController
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class CaptionWindowDecorationTests : ShellTestCase() {
+    @Test
+    fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
+        val taskInfo = createTaskInfo()
+        taskInfo.configuration.windowConfiguration.windowingMode =
+            WindowConfiguration.WINDOWING_MODE_FREEFORM
+        taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance =
+            WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
+        val relayoutParams = WindowDecoration.RelayoutParams()
+
+        CaptionWindowDecoration.updateRelayoutParams(
+            relayoutParams,
+            taskInfo,
+            true,
+            false
+        )
+
+        Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue()
+    }
+
+    @Test
+    fun updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() {
+        val taskInfo = createTaskInfo()
+        taskInfo.configuration.windowConfiguration.windowingMode =
+            WindowConfiguration.WINDOWING_MODE_FREEFORM
+        taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = 0
+        val relayoutParams = WindowDecoration.RelayoutParams()
+
+        CaptionWindowDecoration.updateRelayoutParams(
+            relayoutParams,
+            taskInfo,
+            true,
+            false
+        )
+
+        Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse()
+    }
+
+    @Test
+    fun updateRelayoutParams_addOccludingCaptionElementCorrectly() {
+        val taskInfo = createTaskInfo()
+        val relayoutParams = WindowDecoration.RelayoutParams()
+        CaptionWindowDecoration.updateRelayoutParams(
+            relayoutParams,
+            taskInfo,
+            true,
+            false
+        )
+        Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
+        Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo(
+            WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.START)
+        Truth.assertThat(relayoutParams.mOccludingCaptionElements[1].mAlignment).isEqualTo(
+            WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.END)
+    }
+
+    private fun createTaskInfo(): ActivityManager.RunningTaskInfo {
+        val taskDescriptionBuilder =
+            ActivityManager.TaskDescription.Builder()
+        val taskInfo = TestRunningTaskInfoBuilder()
+            .setDisplayId(Display.DEFAULT_DISPLAY)
+            .setTaskDescriptionBuilder(taskDescriptionBuilder)
+            .setVisible(true)
+            .build()
+        taskInfo.realActivity = ComponentName(
+            "com.android.wm.shell.windowdecor",
+            "CaptionWindowDecorationTests"
+        )
+        taskInfo.baseActivity = ComponentName(
+            "com.android.wm.shell.windowdecor",
+            "CaptionWindowDecorationTests"
+        )
+        return taskInfo
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index e0e603ff..adda9a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -94,6 +94,8 @@
 
     private lateinit var handleMenu: HandleMenu
 
+    private val menuWidthWithElevation = MENU_WIDTH + MENU_PILL_ELEVATION
+
     @Before
     fun setUp() {
         val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer(
@@ -117,6 +119,9 @@
             addOverride(R.dimen.desktop_mode_handle_menu_height, MENU_HEIGHT)
             addOverride(R.dimen.desktop_mode_handle_menu_margin_top, MENU_TOP_MARGIN)
             addOverride(R.dimen.desktop_mode_handle_menu_margin_start, MENU_START_MARGIN)
+            addOverride(R.dimen.desktop_mode_handle_menu_pill_elevation, MENU_PILL_ELEVATION)
+            addOverride(
+                R.dimen.desktop_mode_handle_menu_pill_spacing_margin, MENU_PILL_SPACING_MARGIN)
         }
         mockDesktopWindowDecoration.mDecorWindowContext = mContext
     }
@@ -129,7 +134,7 @@
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of display.
-        val expected = Point(DISPLAY_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN)
+        val expected = Point(DISPLAY_BOUNDS.centerX() - menuWidthWithElevation / 2, MENU_TOP_MARGIN)
         assertEquals(expected.toPointF(), handleMenu.handleMenuPosition)
     }
 
@@ -152,7 +157,10 @@
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of split left task.
-        val expected = Point(SPLIT_LEFT_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN)
+        val expected = Point(
+            SPLIT_LEFT_BOUNDS.centerX() - menuWidthWithElevation / 2,
+            MENU_TOP_MARGIN
+        )
         assertEquals(expected.toPointF(), handleMenu.handleMenuPosition)
     }
 
@@ -164,7 +172,10 @@
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of split right task.
-        val expected = Point(SPLIT_RIGHT_BOUNDS.centerX() - MENU_WIDTH / 2, MENU_TOP_MARGIN)
+        val expected = Point(
+            SPLIT_RIGHT_BOUNDS.centerX() - menuWidthWithElevation / 2,
+            MENU_TOP_MARGIN
+        )
         assertEquals(expected.toPointF(), handleMenu.handleMenuPosition)
     }
 
@@ -220,5 +231,7 @@
         private const val MENU_HEIGHT = 400
         private const val MENU_TOP_MARGIN = 10
         private const val MENU_START_MARGIN = 20
+        private const val MENU_PILL_ELEVATION = 2
+        private const val MENU_PILL_SPACING_MARGIN = 4
     }
 }
diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index 073bc8d..b4e6b72 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -28,6 +28,7 @@
 
 extern int register_android_graphics_Bitmap(JNIEnv*);
 extern int register_android_graphics_BitmapFactory(JNIEnv*);
+extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*);
 extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env);
 extern int register_android_graphics_Camera(JNIEnv* env);
 extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env);
@@ -53,8 +54,11 @@
 extern int register_android_graphics_DrawFilter(JNIEnv* env);
 extern int register_android_graphics_FontFamily(JNIEnv* env);
 extern int register_android_graphics_Gainmap(JNIEnv* env);
+extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
 extern int register_android_graphics_HardwareRendererObserver(JNIEnv* env);
 extern int register_android_graphics_Matrix(JNIEnv* env);
+extern int register_android_graphics_Mesh(JNIEnv* env);
+extern int register_android_graphics_MeshSpecification(JNIEnv* env);
 extern int register_android_graphics_Paint(JNIEnv* env);
 extern int register_android_graphics_Path(JNIEnv* env);
 extern int register_android_graphics_PathIterator(JNIEnv* env);
@@ -87,6 +91,8 @@
 static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
         {"android.graphics.Bitmap", REG_JNI(register_android_graphics_Bitmap)},
         {"android.graphics.BitmapFactory", REG_JNI(register_android_graphics_BitmapFactory)},
+        {"android.graphics.BitmapRegionDecoder",
+         REG_JNI(register_android_graphics_BitmapRegionDecoder)},
         {"android.graphics.ByteBufferStreamAdaptor",
          REG_JNI(register_android_graphics_ByteBufferStreamAdaptor)},
         {"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)},
@@ -101,6 +107,8 @@
         {"android.graphics.FontFamily", REG_JNI(register_android_graphics_FontFamily)},
         {"android.graphics.Gainmap", REG_JNI(register_android_graphics_Gainmap)},
         {"android.graphics.Graphics", REG_JNI(register_android_graphics_Graphics)},
+        {"android.graphics.HardwareBufferRenderer",
+         REG_JNI(register_android_graphics_HardwareBufferRenderer)},
         {"android.graphics.HardwareRenderer", REG_JNI(register_android_view_ThreadedRenderer)},
         {"android.graphics.HardwareRendererObserver",
          REG_JNI(register_android_graphics_HardwareRendererObserver)},
@@ -108,6 +116,9 @@
         {"android.graphics.Interpolator", REG_JNI(register_android_graphics_Interpolator)},
         {"android.graphics.MaskFilter", REG_JNI(register_android_graphics_MaskFilter)},
         {"android.graphics.Matrix", REG_JNI(register_android_graphics_Matrix)},
+        {"android.graphics.Mesh", REG_JNI(register_android_graphics_Mesh)},
+        {"android.graphics.MeshSpecification",
+         REG_JNI(register_android_graphics_MeshSpecification)},
         {"android.graphics.NinePatch", REG_JNI(register_android_graphics_NinePatch)},
         {"android.graphics.Paint", REG_JNI(register_android_graphics_Paint)},
         {"android.graphics.Path", REG_JNI(register_android_graphics_Path)},
diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp
index ae9792d..b943496 100644
--- a/libs/hwui/jni/MeshSpecification.cpp
+++ b/libs/hwui/jni/MeshSpecification.cpp
@@ -126,7 +126,7 @@
     SkSafeUnref(meshSpec);
 }
 
-static jlong getMeshSpecificationFinalizer() {
+static jlong getMeshSpecificationFinalizer(CRITICAL_JNI_PARAMS) {
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&MeshSpecification_safeUnref));
 }
 
diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
index e3cdee6..3b1b861 100644
--- a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp
@@ -135,7 +135,7 @@
     proxy->setLightAlpha((uint8_t)(255 * ambientShadowAlpha), (uint8_t)(255 * spotShadowAlpha));
 }
 
-static jlong android_graphics_HardwareBufferRenderer_getFinalizer() {
+static jlong android_graphics_HardwareBufferRenderer_getFinalizer(CRITICAL_JNI_PARAMS) {
     return static_cast<jlong>(reinterpret_cast<uintptr_t>(&HardwareBufferRenderer_destroy));
 }
 
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 9ce1c82..395f81d 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -963,22 +963,9 @@
             throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
                     + " NFC extras APIs");
         }
-        try {
-            return sService.getNfcDtaInterface(mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return null;
-            }
-            try {
-                return sService.getNfcDtaInterface(mContext.getPackageName());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return null;
-        }
+        return callServiceReturn(() ->  sService.getNfcDtaInterface(mContext.getPackageName()),
+                null);
+
     }
 
     /**
@@ -1095,22 +1082,8 @@
     @SystemApi
     @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
     public @AdapterState int getAdapterState() {
-        try {
-            return sService.getState();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return NfcAdapter.STATE_OFF;
-            }
-            try {
-                return sService.getState();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return NfcAdapter.STATE_OFF;
-        }
+        return callServiceReturn(() ->  sService.getState(), NfcAdapter.STATE_OFF);
+
     }
 
     /**
@@ -1134,22 +1107,8 @@
     @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE)
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean enable() {
-        try {
-            return sService.enable(mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.enable(mContext.getPackageName());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.enable(mContext.getPackageName()), false);
+
     }
 
     /**
@@ -1175,22 +1134,9 @@
     @FlaggedApi(Flags.FLAG_NFC_STATE_CHANGE)
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean disable() {
-        try {
-            return sService.disable(true, mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.disable(true, mContext.getPackageName());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.disable(true, mContext.getPackageName()),
+                false);
+
     }
 
     /**
@@ -1200,22 +1146,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
     public boolean disable(boolean persist) {
-        try {
-            return sService.disable(persist, mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.disable(persist, mContext.getPackageName());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.disable(persist, mContext.getPackageName()),
+                false);
+
     }
 
     /**
@@ -1241,12 +1174,7 @@
      */
     @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
     public boolean isObserveModeSupported() {
-        try {
-            return sService.isObserveModeSupported();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isObserveModeSupported(), false);
     }
 
     /**
@@ -1257,12 +1185,7 @@
 
     @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE)
     public boolean isObserveModeEnabled() {
-        try {
-            return sService.isObserveModeEnabled();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isObserveModeEnabled(), false);
     }
 
     /**
@@ -1286,12 +1209,8 @@
             throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
                     + " observe mode APIs");
         }
-        try {
-            return sService.setObserveMode(enabled, mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            return false;
-        }
+        return callServiceReturn(() ->  sService.setObserveMode(enabled, mContext.getPackageName()),
+                false);
     }
 
     /**
@@ -2057,22 +1976,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.setNfcSecure(enable);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.setNfcSecure(enable);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.setNfcSecure(enable), false);
+
     }
 
     /**
@@ -2088,22 +1993,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.deviceSupportsNfcSecure();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.deviceSupportsNfcSecure();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.deviceSupportsNfcSecure(), false);
+
     }
 
     /**
@@ -2121,22 +2012,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.getNfcAntennaInfo();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return null;
-            }
-            try {
-                return sService.getNfcAntennaInfo();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return null;
-        }
+        return callServiceReturn(() ->  sService.getNfcAntennaInfo(), null);
+
     }
 
     /**
@@ -2154,22 +2031,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isNfcSecureEnabled();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isNfcSecureEnabled();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isNfcSecureEnabled(), false);
+
     }
 
     /**
@@ -2185,22 +2048,8 @@
         if (!sHasNfcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.enableReaderOption(enable);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.enableReaderOption(enable);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.enableReaderOption(enable), false);
+
     }
 
     /**
@@ -2214,22 +2063,8 @@
         if (!sHasNfcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isReaderOptionSupported();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isReaderOptionSupported();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isReaderOptionSupported(), false);
+
     }
 
     /**
@@ -2245,22 +2080,8 @@
         if (!sHasNfcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isReaderOptionEnabled();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isReaderOptionEnabled();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isReaderOptionEnabled(), false);
+
     }
 
     /**
@@ -2388,11 +2209,9 @@
         synchronized (mLock) {
             mTagRemovedListener = iListener;
         }
-        try {
-            return sService.ignore(tag.getServiceHandle(), debounceMs, iListener);
-        } catch (RemoteException e) {
-            return false;
-        }
+        final ITagRemovedCallback.Stub passedListener = iListener;
+        return callServiceReturn(() ->
+                sService.ignore(tag.getServiceHandle(), debounceMs, passedListener), false);
     }
 
     /**
@@ -2509,22 +2328,9 @@
             throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
                     + " NFC extras APIs");
         }
-        try {
-            return sService.getNfcAdapterExtrasInterface(mContext.getPackageName());
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return null;
-            }
-            try {
-                return sService.getNfcAdapterExtrasInterface(mContext.getPackageName());
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return null;
-        }
+        return callServiceReturn(() ->
+                sService.getNfcAdapterExtrasInterface(mContext.getPackageName()), null);
+
     }
 
     void enforceResumed(Activity activity) {
@@ -2569,22 +2375,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.setControllerAlwaysOn(value);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.setControllerAlwaysOn(value);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.setControllerAlwaysOn(value), false);
+
     }
 
     /**
@@ -2600,22 +2392,8 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
     public boolean isControllerAlwaysOn() {
-        try {
-            return sService.isControllerAlwaysOn();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isControllerAlwaysOn();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isControllerAlwaysOn(), false);
+
     }
 
     /**
@@ -2634,22 +2412,8 @@
         if (!sHasNfcFeature && !sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isControllerAlwaysOnSupported();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isControllerAlwaysOnSupported();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isControllerAlwaysOnSupported(), false);
+
     }
 
     /**
@@ -2719,21 +2483,9 @@
             Log.e(TAG, "TagIntentAppPreference is not supported");
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            try {
-                return sService.setTagIntentAppPreferenceForUser(userId, pkg, allow);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE;
-        }
+        return callServiceReturn(() ->
+                sService.setTagIntentAppPreferenceForUser(userId, pkg, allow),
+                        TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE);
     }
 
 
@@ -2808,22 +2560,8 @@
         if (!sHasNfcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isTagIntentAppPreferenceSupported();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isTagIntentAppPreferenceSupported();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isTagIntentAppPreferenceSupported(), false);
+
     }
 
    /**
@@ -2836,26 +2574,10 @@
     @TestApi
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
     public void notifyPollingLoop(@NonNull PollingFrame pollingFrame) {
-        try {
-            if (sService == null) {
-                attemptDeadServiceRecovery(null);
-            }
-            sService.notifyPollingLoop(pollingFrame);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return;
-            }
-            try {
-                sService.notifyPollingLoop(pollingFrame);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-        }
+        callService(() ->  sService.notifyPollingLoop(pollingFrame));
     }
 
+
    /**
      * Notifies the system of new HCE data for tests.
      *
@@ -2863,11 +2585,19 @@
      */
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
     public void notifyTestHceData(int technology, byte[] data) {
+        callService(() ->  sService.notifyTestHceData(technology, data));
+    }
+
+    interface ServiceCall {
+        void call() throws RemoteException;
+    }
+
+    void callService(ServiceCall call) {
         try {
             if (sService == null) {
                 attemptDeadServiceRecovery(null);
             }
-            sService.notifyTestHceData(technology, data);
+            call.call();
         } catch (RemoteException e) {
             attemptDeadServiceRecovery(e);
             // Try one more time
@@ -2876,12 +2606,36 @@
                 return;
             }
             try {
-                sService.notifyTestHceData(technology, data);
-            } catch (RemoteException e2) {
+                call.call();
+            } catch (RemoteException ee) {
                 Log.e(TAG, "Failed to recover NFC Service.");
             }
         }
     }
+    interface ServiceCallReturn<T> {
+        T call() throws RemoteException;
+    }
+    <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) {
+        try {
+            if (sService == null) {
+                attemptDeadServiceRecovery(null);
+            }
+            return call.call();
+        } catch (RemoteException e) {
+            attemptDeadServiceRecovery(e);
+            // Try one more time
+            if (sService == null) {
+                Log.e(TAG, "Failed to recover NFC Service.");
+                return defaultReturn;
+            }
+            try {
+                return call.call();
+            } catch (RemoteException ee) {
+                Log.e(TAG, "Failed to recover NFC Service.");
+            }
+        }
+        return defaultReturn;
+    }
 
    /**
      * Notifies the system of a an HCE session being deactivated.
@@ -2891,24 +2645,7 @@
     @TestApi
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
     public void notifyHceDeactivated() {
-        try {
-            if (sService == null) {
-                attemptDeadServiceRecovery(null);
-            }
-            sService.notifyHceDeactivated();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return;
-            }
-            try {
-                sService.notifyHceDeactivated();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-        }
+        callService(() ->  sService.notifyHceDeactivated());
     }
 
     /**
@@ -2924,22 +2661,7 @@
         if (!sHasNfcWlcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.setWlcEnabled(enable);
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.setWlcEnabled(enable);
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.setWlcEnabled(enable), false);
     }
 
     /**
@@ -2954,22 +2676,8 @@
         if (!sHasNfcWlcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.isWlcEnabled();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return false;
-            }
-            try {
-                return sService.isWlcEnabled();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return false;
-        }
+        return callServiceReturn(() ->  sService.isWlcEnabled(), false);
+
     }
 
     /**
@@ -3048,22 +2756,8 @@
         if (!sHasNfcWlcFeature) {
             throw new UnsupportedOperationException();
         }
-        try {
-            return sService.getWlcListenerDeviceInfo();
-        } catch (RemoteException e) {
-            attemptDeadServiceRecovery(e);
-            // Try one more time
-            if (sService == null) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-                return null;
-            }
-            try {
-                return sService.getWlcListenerDeviceInfo();
-            } catch (RemoteException ee) {
-                Log.e(TAG, "Failed to recover NFC Service.");
-            }
-            return null;
-        }
+        return callServiceReturn(() ->  sService.getWlcListenerDeviceInfo(), null);
+
     }
 
     /**
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index bbf0315..4387b6f 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.widget;
 
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -122,6 +124,8 @@
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
 
+        final FrameLayout illustrationFrame = (FrameLayout) holder.findViewById(
+                R.id.illustration_frame);
         final ImageView backgroundView =
                 (ImageView) holder.findViewById(R.id.background_view);
         final FrameLayout middleGroundLayout =
@@ -130,15 +134,15 @@
                 (LottieAnimationView) holder.findViewById(R.id.lottie_view);
         if (illustrationView != null && !TextUtils.isEmpty(mContentDescription)) {
             illustrationView.setContentDescription(mContentDescription);
-            illustrationView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            illustrationView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            final View illustrationContainer = (View) illustrationFrame.getParent();
+            illustrationContainer.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
         }
         // To solve the problem of non-compliant illustrations, we set the frame height
         // to 300dp and set the length of the short side of the screen to
         // the width of the frame.
         final int screenWidth = getContext().getResources().getDisplayMetrics().widthPixels;
         final int screenHeight = getContext().getResources().getDisplayMetrics().heightPixels;
-        final FrameLayout illustrationFrame = (FrameLayout) holder.findViewById(
-                R.id.illustration_frame);
         final LayoutParams lp = (LayoutParams) illustrationFrame.getLayoutParams();
         lp.width = screenWidth < screenHeight ? screenWidth : screenHeight;
         illustrationFrame.setLayoutParams(lp);
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt
index 14af508..2965793 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.spa.debug
 
-import android.net.Uri
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
@@ -41,8 +40,6 @@
 import com.android.settingslib.spa.framework.util.SESSION_BROWSE
 import com.android.settingslib.spa.framework.util.SESSION_SEARCH
 import com.android.settingslib.spa.framework.util.createIntent
-import com.android.settingslib.spa.slice.fromEntry
-import com.android.settingslib.spa.slice.presenter.SliceDemo
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -52,7 +49,6 @@
 private const val ROUTE_ROOT = "root"
 private const val ROUTE_All_PAGES = "pages"
 private const val ROUTE_All_ENTRIES = "entries"
-private const val ROUTE_All_SLICES = "slices"
 private const val ROUTE_PAGE = "page"
 private const val ROUTE_ENTRY = "entry"
 private const val PARAM_NAME_PAGE_ID = "pid"
@@ -87,7 +83,6 @@
                 composable(route = ROUTE_ROOT) { RootPage() }
                 composable(route = ROUTE_All_PAGES) { AllPages() }
                 composable(route = ROUTE_All_ENTRIES) { AllEntries() }
-                composable(route = ROUTE_All_SLICES) { AllSlices() }
                 composable(
                     route = "$ROUTE_PAGE/{$PARAM_NAME_PAGE_ID}",
                     arguments = listOf(
@@ -109,8 +104,6 @@
         val entryRepository by spaEnvironment.entryRepository
         val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
         val allEntry = remember { entryRepository.getAllEntries() }
-        val allSliceEntry =
-            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
         HomeScaffold(title = "Settings Debug") {
             Preference(object : PreferenceModel {
                 override val title = "List All Pages (${allPageWithEntry.size})"
@@ -120,10 +113,6 @@
                 override val title = "List All Entries (${allEntry.size})"
                 override val onClick = navigator(route = ROUTE_All_ENTRIES)
             })
-            Preference(object : PreferenceModel {
-                override val title = "List All Slices (${allSliceEntry.size})"
-                override val onClick = navigator(route = ROUTE_All_SLICES)
-            })
         }
     }
 
@@ -152,18 +141,6 @@
         }
     }
 
-    @Composable
-    fun AllSlices() {
-        val entryRepository by spaEnvironment.entryRepository
-        val authority = spaEnvironment.sliceProviderAuthorities
-        val allSliceEntry =
-            remember { entryRepository.getAllEntries().filter { it.hasSliceSupport } }
-        RegularScaffold(title = "All Slices (${allSliceEntry.size})") {
-            for (entry in allSliceEntry) {
-                SliceDemo(sliceUri = Uri.Builder().fromEntry(entry, authority).build())
-            }
-        }
-    }
 
     @Composable
     fun OnePage(arguments: Bundle?) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
index 444a3f0..06d105b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugFormat.kt
@@ -70,7 +70,6 @@
         "allowSearch = $isAllowSearch",
         "isSearchDynamic = $isSearchDataDynamic",
         "isSearchMutable = $hasMutableStatus",
-        "hasSlice = $hasSliceSupport",
         "------ SEARCH ------",
         "search_path = $entryPathWithTitle",
         searchData?.debugContent() ?: "no search data",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
index 780933d3c..e5bbb8f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchContract.kt
@@ -50,7 +50,6 @@
     INTENT_TARGET_PACKAGE("intentTargetPackage"),
     INTENT_TARGET_CLASS("intentTargetClass"),
     INTENT_EXTRAS("intentExtras"),
-    SLICE_URI("sliceUri"),
     ENTRY_DISABLED("entryDisabled"),
 }
 
@@ -71,7 +70,6 @@
             ColumnEnum.INTENT_TARGET_PACKAGE,
             ColumnEnum.INTENT_TARGET_CLASS,
             ColumnEnum.INTENT_EXTRAS,
-            ColumnEnum.SLICE_URI,
         )
     ),
     SEARCH_DYNAMIC_DATA_QUERY(
@@ -85,7 +83,6 @@
             ColumnEnum.INTENT_TARGET_PACKAGE,
             ColumnEnum.INTENT_TARGET_CLASS,
             ColumnEnum.INTENT_EXTRAS,
-            ColumnEnum.SLICE_URI,
         )
     ),
     SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
@@ -115,7 +112,6 @@
             ColumnEnum.INTENT_TARGET_PACKAGE,
             ColumnEnum.INTENT_TARGET_CLASS,
             ColumnEnum.INTENT_EXTRAS,
-            ColumnEnum.SLICE_URI,
             ColumnEnum.ENTRY_DISABLED,
         )
     ),
@@ -130,7 +126,6 @@
             ColumnEnum.INTENT_TARGET_PACKAGE,
             ColumnEnum.INTENT_TARGET_CLASS,
             ColumnEnum.INTENT_EXTRAS,
-            ColumnEnum.SLICE_URI,
             ColumnEnum.ENTRY_DISABLED,
         )
     ),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
index eacb28c..65f700c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/search/SpaSearchProvider.kt
@@ -32,8 +32,6 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.util.SESSION_SEARCH
 import com.android.settingslib.spa.framework.util.createIntent
-import com.android.settingslib.spa.slice.fromEntry
-
 
 private const val TAG = "SpaSearchProvider"
 
@@ -217,11 +215,6 @@
                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
         }
-        if (entry.hasSliceSupport)
-            row.add(
-                ColumnEnum.SLICE_URI.id, Uri.Builder()
-                    .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
-            )
     }
 
     private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
@@ -252,11 +245,6 @@
                 .add(ColumnEnum.INTENT_TARGET_CLASS.id, spaEnvironment.browseActivityClass?.name)
                 .add(ColumnEnum.INTENT_EXTRAS.id, marshall(intent.extras))
         }
-        if (entry.hasSliceSupport)
-            row.add(
-                ColumnEnum.SLICE_URI.id, Uri.Builder()
-                    .fromEntry(entry, spaEnvironment.sliceProviderAuthorities)
-            )
         // Fetch status data. We can add runtime arguments later if necessary
         val statusData = entry.getStatusData() ?: return
         row.add(ColumnEnum.ENTRY_DISABLED.id, statusData.isDisabled)
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index 8868837..4028b73 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -105,12 +105,6 @@
         public static final String COLUMN_CARD_STATE = "cardState";
 
         /**
-         * The name of the extended APDU supported state column, see
-         * {@link UiccSlotInfo#getIsExtendedApduSupported()}.
-         */
-        public static final String COLUMN_IS_EXTENDED_APDU_SUPPORTED = "isExtendedApduSupported";
-
-        /**
          * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}.
          */
         public static final String COLUMN_IS_REMOVABLE = "isRemovable";
@@ -150,74 +144,17 @@
         public static final String COLUMN_SIM_SLOT_INDEX = "simSlotIndex";
 
         /**
-         * The name of the carrier ID column, see {@link SubscriptionInfo#getCarrierId()}.
-         */
-        public static final String COLUMN_CARRIER_ID = "carrierId";
-
-        /**
-         * The name of the display name column, see {@link SubscriptionInfo#getDisplayName()}.
-         */
-        public static final String COLUMN_DISPLAY_NAME = "displayName";
-
-        /**
-         * The name of the carrier name column, see {@link SubscriptionInfo#getCarrierName()}.
-         */
-        public static final String COLUMN_CARRIER_NAME = "carrierName";
-
-        /**
-         * The name of the data roaming state column, see
-         * {@link SubscriptionInfo#getDataRoaming()}.
-         */
-        public static final String COLUMN_DATA_ROAMING = "dataRoaming";
-
-        /**
-         * The name of the mcc column, see {@link SubscriptionInfo#getMccString()}.
-         */
-        public static final String COLUMN_MCC = "mcc";
-
-        /**
-         * The name of the mnc column, see {@link SubscriptionInfo#getMncString()}.
-         */
-        public static final String COLUMN_MNC = "mnc";
-
-        /**
-         * The name of the country ISO column, see {@link SubscriptionInfo#getCountryIso()}.
-         */
-        public static final String COLUMN_COUNTRY_ISO = "countryIso";
-
-        /**
          * The name of the embedded state column, see {@link SubscriptionInfo#isEmbedded()}.
          */
         public static final String COLUMN_IS_EMBEDDED = "isEmbedded";
 
         /**
-         * The name of the card ID column, see {@link SubscriptionInfo#getCardId()}.
-         */
-        public static final String COLUMN_CARD_ID = "cardId";
-
-        /**
-         * The name of the port index column, see {@link SubscriptionInfo#getPortIndex()}.
-         */
-        public static final String COLUMN_PORT_INDEX = "portIndex";
-
-        /**
          * The name of the opportunistic state column, see
          * {@link SubscriptionInfo#isOpportunistic()}.
          */
         public static final String COLUMN_IS_OPPORTUNISTIC = "isOpportunistic";
 
         /**
-         * The name of the groupUUID column, see {@link SubscriptionInfo#getGroupUuid()}.
-         */
-        public static final String COLUMN_GROUP_UUID = "groupUUID";
-
-        /**
-         * The name of the subscription type column, see
-         * {@link SubscriptionInfo#getSubscriptionType()}}.
-         */
-        public static final String COLUMN_SUBSCRIPTION_TYPE = "subscriptionType";
-
-        /**
          * The name of the uniqueName column,
          * {@see SubscriptionUtil#getUniqueSubscriptionDisplayName(SubscriptionInfo, Context)}.
          */
@@ -231,19 +168,6 @@
         public static final String COLUMN_IS_SUBSCRIPTION_VISIBLE = "isSubscriptionVisible";
 
         /**
-         * The name of the formatted phone number column,
-         * {@see SubscriptionUtil#getFormattedPhoneNumber(Context, SubscriptionInfo)}.
-         */
-        public static final String COLUMN_FORMATTED_PHONE_NUMBER = "getFormattedPhoneNumber";
-
-        /**
-         * The name of the first removable subscription state column,
-         * {@see SubscriptionUtil#getFirstRemovableSubscription(Context)}.
-         */
-        public static final String COLUMN_IS_FIRST_REMOVABLE_SUBSCRIPTION =
-                "isFirstRemovableSubscription";
-
-        /**
          * The name of the default subscription selection column,
          * {@see SubscriptionUtil#getSubscriptionOrDefault(Context, int)}.
          */
@@ -257,24 +181,12 @@
         public static final String COLUMN_IS_VALID_SUBSCRIPTION = "isValidSubscription";
 
         /**
-         * The name of the usable subscription column,
-         * {@link SubscriptionManager#isUsableSubscriptionId(int)}.
-         */
-        public static final String COLUMN_IS_USABLE_SUBSCRIPTION = "isUsableSubscription";
-
-        /**
          * The name of the active subscription column,
          * {@link SubscriptionManager#isActiveSubscriptionId(int)}.
          */
         public static final String COLUMN_IS_ACTIVE_SUBSCRIPTION_ID = "isActiveSubscription";
 
         /**
-         * The name of the available subscription column,
-         * {@see SubscriptionUtil#getAvailableSubscription(Context, ProxySubscriptionManager, int)}.
-         */
-        public static final String COLUMN_IS_AVAILABLE_SUBSCRIPTION = "isAvailableSubscription";
-
-        /**
          * The name of the active data subscription state column, see
          * {@link SubscriptionManager#getActiveDataSubscriptionId()}.
          */
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
index e6b1cfb..060eab66 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java
@@ -40,15 +40,6 @@
             + DataServiceUtils.SubscriptionInfoData.COLUMN_ID + " = :subId")
     SubscriptionInfoEntity querySubInfoById(String subId);
 
-    @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE "
-            + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID
-            + " = :isActiveSubscription" + " AND "
-            + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_SUBSCRIPTION_VISIBLE
-            + " = :isSubscriptionVisible" + " ORDER BY "
-            + DataServiceUtils.SubscriptionInfoData.COLUMN_SIM_SLOT_INDEX)
-    LiveData<List<SubscriptionInfoEntity>> queryActiveSubInfos(
-            boolean isActiveSubscription, boolean isSubscriptionVisible);
-
     @Query("SELECT COUNT(*) FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME)
     int count();
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
index 361a2461..88e6a57 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
@@ -19,7 +19,6 @@
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.room.ColumnInfo;
 import androidx.room.Entity;
 import androidx.room.PrimaryKey;
@@ -28,39 +27,19 @@
 
 @Entity(tableName = DataServiceUtils.SubscriptionInfoData.TABLE_NAME)
 public class SubscriptionInfoEntity {
-    public SubscriptionInfoEntity(@NonNull String subId, int simSlotIndex, int carrierId,
-            String displayName, String carrierName, int dataRoaming, String mcc, String mnc,
-            String countryIso, boolean isEmbedded, int cardId, int portIndex,
-            boolean isOpportunistic, @Nullable String groupUUID, int subscriptionType,
-            String uniqueName, boolean isSubscriptionVisible, @Nullable String formattedPhoneNumber,
-            boolean isFirstRemovableSubscription, boolean isDefaultSubscriptionSelection,
-            boolean isValidSubscription, boolean isUsableSubscription,
-            boolean isActiveSubscriptionId, boolean isAvailableSubscription,
-            boolean isActiveDataSubscriptionId) {
+    public SubscriptionInfoEntity(@NonNull String subId, int simSlotIndex, boolean isEmbedded,
+            boolean isOpportunistic, String uniqueName, boolean isSubscriptionVisible,
+            boolean isDefaultSubscriptionSelection, boolean isValidSubscription,
+            boolean isActiveSubscriptionId, boolean isActiveDataSubscriptionId) {
         this.subId = subId;
         this.simSlotIndex = simSlotIndex;
-        this.carrierId = carrierId;
-        this.displayName = displayName;
-        this.carrierName = carrierName;
-        this.dataRoaming = dataRoaming;
-        this.mcc = mcc;
-        this.mnc = mnc;
-        this.countryIso = countryIso;
         this.isEmbedded = isEmbedded;
-        this.cardId = cardId;
-        this.portIndex = portIndex;
         this.isOpportunistic = isOpportunistic;
-        this.groupUUID = groupUUID;
-        this.subscriptionType = subscriptionType;
         this.uniqueName = uniqueName;
         this.isSubscriptionVisible = isSubscriptionVisible;
-        this.formattedPhoneNumber = formattedPhoneNumber;
-        this.isFirstRemovableSubscription = isFirstRemovableSubscription;
         this.isDefaultSubscriptionSelection = isDefaultSubscriptionSelection;
         this.isValidSubscription = isValidSubscription;
-        this.isUsableSubscription = isUsableSubscription;
         this.isActiveSubscriptionId = isActiveSubscriptionId;
-        this.isAvailableSubscription = isAvailableSubscription;
         this.isActiveDataSubscriptionId = isActiveDataSubscriptionId;
     }
 
@@ -72,59 +51,18 @@
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_SIM_SLOT_INDEX)
     public int simSlotIndex;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARRIER_ID)
-    public int carrierId;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_DISPLAY_NAME)
-    public String displayName;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARRIER_NAME)
-    public String carrierName;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_DATA_ROAMING)
-    public int dataRoaming;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_MCC)
-    public String mcc;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_MNC)
-    public String mnc;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_COUNTRY_ISO)
-    public String countryIso;
-
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_EMBEDDED)
     public boolean isEmbedded;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_CARD_ID)
-    public int cardId;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_PORT_INDEX)
-    public int portIndex;
-
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_OPPORTUNISTIC)
     public boolean isOpportunistic;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_GROUP_UUID)
-    @Nullable
-    public String groupUUID;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_SUBSCRIPTION_TYPE)
-    public int subscriptionType;
-
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_UNIQUE_NAME)
     public String uniqueName;
 
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_SUBSCRIPTION_VISIBLE)
     public boolean isSubscriptionVisible;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_FORMATTED_PHONE_NUMBER)
-    @Nullable
-    public String formattedPhoneNumber;
-
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_FIRST_REMOVABLE_SUBSCRIPTION)
-    public boolean isFirstRemovableSubscription;
-
     @ColumnInfo(name =
             DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION_SELECTION)
     public boolean isDefaultSubscriptionSelection;
@@ -132,15 +70,9 @@
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_VALID_SUBSCRIPTION)
     public boolean isValidSubscription;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_USABLE_SUBSCRIPTION)
-    public boolean isUsableSubscription;
-
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID)
     public boolean isActiveSubscriptionId;
 
-    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_AVAILABLE_SUBSCRIPTION)
-    public boolean isAvailableSubscription;
-
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION)
     public boolean isActiveDataSubscriptionId;
 
@@ -165,28 +97,13 @@
         return Objects.hash(
                 subId,
                 simSlotIndex,
-                carrierId,
-                displayName,
-                carrierName,
-                dataRoaming,
-                mcc,
-                mnc,
-                countryIso,
                 isEmbedded,
-                cardId,
-                portIndex,
                 isOpportunistic,
-                groupUUID,
-                subscriptionType,
                 uniqueName,
                 isSubscriptionVisible,
-                formattedPhoneNumber,
-                isFirstRemovableSubscription,
                 isDefaultSubscriptionSelection,
                 isValidSubscription,
-                isUsableSubscription,
                 isActiveSubscriptionId,
-                isAvailableSubscription,
                 isActiveDataSubscriptionId);
     }
 
@@ -202,28 +119,13 @@
         SubscriptionInfoEntity info = (SubscriptionInfoEntity) obj;
         return  TextUtils.equals(subId, info.subId)
                 && simSlotIndex == info.simSlotIndex
-                && carrierId == info.carrierId
-                && TextUtils.equals(displayName, info.displayName)
-                && TextUtils.equals(carrierName, info.carrierName)
-                && dataRoaming == info.dataRoaming
-                && TextUtils.equals(mcc, info.mcc)
-                && TextUtils.equals(mnc, info.mnc)
-                && TextUtils.equals(countryIso, info.countryIso)
                 && isEmbedded == info.isEmbedded
-                && cardId == info.cardId
-                && portIndex == info.portIndex
                 && isOpportunistic == info.isOpportunistic
-                && TextUtils.equals(groupUUID, info.groupUUID)
-                && subscriptionType == info.subscriptionType
                 && TextUtils.equals(uniqueName, info.uniqueName)
                 && isSubscriptionVisible == info.isSubscriptionVisible
-                && TextUtils.equals(formattedPhoneNumber, info.formattedPhoneNumber)
-                && isFirstRemovableSubscription == info.isFirstRemovableSubscription
                 && isDefaultSubscriptionSelection == info.isDefaultSubscriptionSelection
                 && isValidSubscription == info.isValidSubscription
-                && isUsableSubscription == info.isUsableSubscription
                 && isActiveSubscriptionId == info.isActiveSubscriptionId
-                && isAvailableSubscription == info.isAvailableSubscription
                 && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId;
     }
 
@@ -233,50 +135,20 @@
                 .append(subId)
                 .append(", simSlotIndex = ")
                 .append(simSlotIndex)
-                .append(", carrierId = ")
-                .append(carrierId)
-                .append(", displayName = ")
-                .append(displayName)
-                .append(", carrierName = ")
-                .append(carrierName)
-                .append(", dataRoaming = ")
-                .append(dataRoaming)
-                .append(", mcc = ")
-                .append(mcc)
-                .append(", mnc = ")
-                .append(mnc)
-                .append(", countryIso = ")
-                .append(countryIso)
                 .append(", isEmbedded = ")
                 .append(isEmbedded)
-                .append(", cardId = ")
-                .append(cardId)
-                .append(", portIndex = ")
-                .append(portIndex)
                 .append(", isOpportunistic = ")
                 .append(isOpportunistic)
-                .append(", groupUUID = ")
-                .append(groupUUID)
-                .append(", subscriptionType = ")
-                .append(subscriptionType)
                 .append(", uniqueName = ")
                 .append(uniqueName)
                 .append(", isSubscriptionVisible = ")
                 .append(isSubscriptionVisible)
-                .append(", formattedPhoneNumber = ")
-                .append(formattedPhoneNumber)
-                .append(", isFirstRemovableSubscription = ")
-                .append(isFirstRemovableSubscription)
                 .append(", isDefaultSubscriptionSelection = ")
                 .append(isDefaultSubscriptionSelection)
                 .append(", isValidSubscription = ")
                 .append(isValidSubscription)
-                .append(", isUsableSubscription = ")
-                .append(isUsableSubscription)
                 .append(", isActiveSubscriptionId = ")
                 .append(isActiveSubscriptionId)
-                .append(", isAvailableSubscription = ")
-                .append(isAvailableSubscription)
                 .append(", isActiveDataSubscriptionId = ")
                 .append(isActiveDataSubscriptionId)
                 .append(")}");
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 8b33afe..1871873 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -581,7 +581,6 @@
         "androidx.activity_activity-compose",
         "androidx.compose.animation_animation-graphics",
         "androidx.lifecycle_lifecycle-viewmodel-compose",
-        "device_policy_aconfig_flags_lib",
     ],
     libs: [
         "keepanno-annotations",
@@ -669,6 +668,7 @@
     ],
     asset_dirs: [
         "tests/goldens",
+        "schemas",
     ],
     static_libs: [
         "SystemUI-res",
@@ -708,6 +708,7 @@
         "androidx-constraintlayout_constraintlayout",
         "androidx.exifinterface_exifinterface",
         "androidx.room_room-runtime",
+        "androidx.room_room-testing",
         "androidx.room_room-ktx",
         "androidx.datastore_datastore-preferences",
         "device_state_flags_lib",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 63ce7eb..71f5511 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -591,6 +591,16 @@
 }
 
 flag {
+    name: "screenshot_save_image_exporter"
+    namespace: "systemui"
+    description: "Save all screenshots using ImageExporter"
+    bug: "352308052"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
    name: "run_fingerprint_detect_on_dismissible_keyguard"
    namespace: "systemui"
    description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
@@ -1153,3 +1163,27 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "sounddose_customization"
+  namespace: "systemui"
+  description: "Enables custom actions for sounddose notifications"
+  bug: "345227709"
+}
+
+flag {
+  namespace: "systemui"
+  name: "register_content_observers_async"
+  description: "Use new Async API to register content observers"
+  bug: "316922634"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
+  name: "msdl_feedback"
+  namespace: "systemui"
+  description: "Enables MSDL feedback in SysUI surfaces."
+  bug: "352600066"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index f6535ec0..8f247f6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -94,6 +94,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -171,7 +172,11 @@
     var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
     var toolbarSize: IntSize? by remember { mutableStateOf(null) }
     var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
-    val gridState = rememberLazyGridState()
+
+    val gridState =
+        rememberLazyGridState(viewModel.savedFirstScrollIndex, viewModel.savedFirstScrollOffset)
+    viewModel.clearPersistedScrollPosition()
+
     val contentListState = rememberContentListState(widgetConfigurator, communalContent, viewModel)
     val reorderingWidgets by viewModel.reorderingWidgets.collectAsStateWithLifecycle()
     val selectedKey = viewModel.selectedKey.collectAsStateWithLifecycle()
@@ -187,6 +192,8 @@
     val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
     val contentOffset = beforeContentPadding(contentPadding).toOffset()
 
+    ObserveScrollEffect(gridState, viewModel)
+
     if (!viewModel.isEditMode) {
         ScrollOnUpdatedLiveContentEffect(communalContent, gridState)
     }
@@ -420,6 +427,20 @@
     }
 }
 
+@Composable
+private fun ObserveScrollEffect(
+    gridState: LazyGridState,
+    communalViewModel: BaseCommunalViewModel
+) {
+
+    LaunchedEffect(gridState) {
+        snapshotFlow {
+                Pair(gridState.firstVisibleItemIndex, gridState.firstVisibleItemScrollOffset)
+            }
+            .collect { communalViewModel.onScrollPositionUpdated(it.first, it.second) }
+    }
+}
+
 /**
  * Observes communal content and scrolls to any added or updated live content, e.g. a new media
  * session is started, or a paused timer is resumed.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
index 166aa70..f9e2252 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt
@@ -28,7 +28,6 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
-import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.height
 import com.android.keyguard.dagger.KeyguardStatusBarViewComponent
@@ -83,29 +82,20 @@
                 componentFactory.build(view, provider).keyguardStatusBarViewController
             }
 
-        MovableElement(
-            key = StatusBarElementKey,
-            modifier = modifier,
-        ) {
-            content {
-                AndroidView(
-                    factory = {
-                        notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
-                            (it.parent as ViewGroup).removeView(it)
-                        }
+        AndroidView(
+            factory = {
+                notificationPanelView.get().findViewById<View>(R.id.keyguard_header)?.let {
+                    (it.parent as ViewGroup).removeView(it)
+                }
 
-                        viewController.init()
-                        view
-                    },
-                    modifier =
-                        Modifier.fillMaxWidth().padding(horizontal = 16.dp).height {
-                            Utils.getStatusBarHeaderHeightKeyguard(context)
-                        },
-                    update = { viewController.setDisplayCutout(viewDisplayCutout) }
-                )
-            }
-        }
+                viewController.init()
+                view
+            },
+            modifier =
+                Modifier.fillMaxWidth().padding(horizontal = 16.dp).height {
+                    Utils.getStatusBarHeaderHeightKeyguard(context)
+                },
+            update = { viewController.setDisplayCutout(viewDisplayCutout) }
+        )
     }
 }
-
-private val StatusBarElementKey = ElementKey("StatusBar")
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index 581f3a5..d629eec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.media.controls.ui.composable
 
+import android.view.View
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.Composable
@@ -26,7 +28,6 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.viewinterop.AndroidView
-import androidx.core.view.contains
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
@@ -36,7 +37,8 @@
 
 private object MediaCarousel {
     object Elements {
-        internal val Content = ElementKey("MediaCarouselContent")
+        internal val Content =
+            ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker)
     }
 }
 
@@ -61,40 +63,43 @@
     mediaHost.measurementInput = MeasurementInput(layoutWidth, layoutHeight)
     carouselController.setSceneContainerSize(layoutWidth, layoutHeight)
 
-    AndroidView(
-        modifier =
-            modifier
-                .element(MediaCarousel.Elements.Content)
-                .height(mediaHeight)
-                .fillMaxWidth()
-                .layout { measurable, constraints ->
-                    val placeable = measurable.measure(constraints)
+    MovableElement(
+        key = MediaCarousel.Elements.Content,
+        modifier = modifier.height(mediaHeight).fillMaxWidth()
+    ) {
+        content {
+            AndroidView(
+                modifier =
+                    Modifier.fillMaxSize().layout { measurable, constraints ->
+                        val placeable = measurable.measure(constraints)
 
-                    // Notify controller to size the carousel for the current space
-                    mediaHost.measurementInput = MeasurementInput(placeable.width, placeable.height)
-                    carouselController.setSceneContainerSize(placeable.width, placeable.height)
+                        // Notify controller to size the carousel for the current space
+                        mediaHost.measurementInput =
+                            MeasurementInput(placeable.width, placeable.height)
+                        carouselController.setSceneContainerSize(placeable.width, placeable.height)
 
-                    layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
+                        layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
+                    },
+                factory = { context ->
+                    FrameLayout(context).apply {
+                        layoutParams =
+                            FrameLayout.LayoutParams(
+                                FrameLayout.LayoutParams.MATCH_PARENT,
+                                FrameLayout.LayoutParams.MATCH_PARENT,
+                            )
+                    }
                 },
-        factory = { context ->
-            FrameLayout(context).apply {
-                val mediaFrame = carouselController.mediaFrame
-                (mediaFrame.parent as? ViewGroup)?.removeView(mediaFrame)
-                addView(mediaFrame)
-                layoutParams =
-                    FrameLayout.LayoutParams(
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                        FrameLayout.LayoutParams.MATCH_PARENT,
-                    )
-            }
-        },
-        update = {
-            if (it.contains(carouselController.mediaFrame)) {
-                return@AndroidView
-            }
-            val mediaFrame = carouselController.mediaFrame
-            (mediaFrame.parent as? ViewGroup)?.removeView(mediaFrame)
-            it.addView(mediaFrame)
-        },
-    )
+                update = { it.setView(carouselController.mediaFrame) },
+                onRelease = { it.removeAllViews() }
+            )
+        }
+    }
+}
+
+private fun ViewGroup.setView(view: View) {
+    if (view.parent == this) {
+        return
+    }
+    (view.parent as? ViewGroup)?.removeView(view)
+    addView(view)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
new file mode 100644
index 0000000..0398133
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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.controls.ui.composable
+
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementScenePicker
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
+
+/** [ElementScenePicker] implementation for the media carousel object. */
+object MediaScenePicker : ElementScenePicker {
+
+    private val shadeLockscreenFraction = 0.65f
+    private val scenes =
+        setOf(
+            Scenes.Lockscreen,
+            Scenes.Shade,
+            Scenes.QuickSettings,
+            Scenes.QuickSettingsShade,
+            Scenes.Communal
+        )
+
+    override fun sceneDuringTransition(
+        element: ElementKey,
+        transition: TransitionState.Transition,
+        fromSceneZIndex: Float,
+        toSceneZIndex: Float
+    ): SceneKey? {
+        return when {
+            // TODO: 352052894 - update with the actual scene picking
+            transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
+                if (transition.progress < shadeLockscreenFraction) {
+                    Scenes.Lockscreen
+                } else {
+                    Scenes.Shade
+                }
+            }
+
+            // TODO: 345467290 - update with the actual scene picking
+            transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
+                if (transition.progress < 1f - shadeLockscreenFraction) {
+                    Scenes.Shade
+                } else {
+                    Scenes.Lockscreen
+                }
+            }
+
+            // TODO: 345467290 - update with the actual scene picking
+            transition.isTransitioningBetween(Scenes.QuickSettings, Scenes.Shade) -> {
+                Scenes.QuickSettings
+            }
+
+            // TODO: 340216785 - update with the actual scene picking
+            else -> pickSingleSceneIn(scenes, transition, element)
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 4a6599a..805351e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -34,9 +34,11 @@
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
@@ -51,6 +53,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInteropFilter
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalDensity
@@ -60,6 +63,7 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexScenePicker
+import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.TransitionState
 import com.android.compose.animation.scene.UserAction
@@ -255,6 +259,8 @@
     val mediaOffset by
         animateSceneDpAsState(value = InQQS, key = MediaLandscapeTopOffset, canOverflow = false)
 
+    val navBarHeight = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()
+
     Box(
         modifier =
             modifier.thenIf(shouldPunchHoleBehindScrim) {
@@ -358,11 +364,22 @@
                 notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt())
             }
         }
-        NotificationStackCutoffGuideline(
-            stackScrollView = notificationStackScrollView,
-            viewModel = notificationsPlaceholderViewModel,
-            modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding()
-        )
+        Box(
+            modifier =
+                Modifier.align(Alignment.BottomCenter)
+                    .height(navBarHeight)
+                    .pointerInteropFilter { true }
+                    .verticalNestedScrollToScene(
+                        topBehavior = NestedScrollBehavior.EdgeAlways,
+                        isExternalOverscrollGesture = { false }
+                    )
+        ) {
+            NotificationStackCutoffGuideline(
+                stackScrollView = notificationStackScrollView,
+                viewModel = notificationsPlaceholderViewModel,
+                modifier = Modifier.align(Alignment.TopCenter)
+            )
+        }
     }
 }
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index cdcfc84..615d393 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -241,43 +241,50 @@
                 }
             }
 
-            awaitPointerEventScope {
-                while (isActive) {
-                    try {
-                        detectDragGestures(
-                            orientation = orientation,
-                            startDragImmediately = startDragImmediately,
-                            onDragStart = { startedPosition, overSlop, pointersDown ->
-                                velocityTracker.resetTracking()
-                                onDragStarted(startedPosition, overSlop, pointersDown)
-                            },
-                            onDrag = { controller, change, amount ->
-                                velocityTracker.addPointerInputChange(change)
-                                controller.onDrag(amount)
-                            },
-                            onDragEnd = { controller ->
-                                val viewConfiguration = currentValueOf(LocalViewConfiguration)
-                                val maxVelocity =
-                                    viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) }
-                                val velocity = velocityTracker.calculateVelocity(maxVelocity)
-                                controller.onStop(
-                                    velocity =
-                                        when (orientation) {
-                                            Orientation.Horizontal -> velocity.x
-                                            Orientation.Vertical -> velocity.y
-                                        },
-                                    canChangeScene = true,
-                                )
-                            },
-                            onDragCancel = { controller ->
-                                controller.onStop(velocity = 0f, canChangeScene = true)
-                            },
-                            swipeDetector = swipeDetector
-                        )
-                    } catch (exception: CancellationException) {
-                        // If the coroutine scope is active, we can just restart the drag cycle.
-                        if (!isActive) {
-                            throw exception
+            // The order is important here: we want to make sure that the previous PointerEventScope
+            // is initialized first. This ensures that the following PointerEventScope doesn't
+            // receive more events than the first one.
+            launch {
+                awaitPointerEventScope {
+                    while (isActive) {
+                        try {
+                            detectDragGestures(
+                                orientation = orientation,
+                                startDragImmediately = startDragImmediately,
+                                onDragStart = { startedPosition, overSlop, pointersDown ->
+                                    velocityTracker.resetTracking()
+                                    onDragStarted(startedPosition, overSlop, pointersDown)
+                                },
+                                onDrag = { controller, change, amount ->
+                                    velocityTracker.addPointerInputChange(change)
+                                    controller.onDrag(amount)
+                                },
+                                onDragEnd = { controller ->
+                                    val viewConfiguration = currentValueOf(LocalViewConfiguration)
+                                    val maxVelocity =
+                                        viewConfiguration.maximumFlingVelocity.let {
+                                            Velocity(it, it)
+                                        }
+                                    val velocity = velocityTracker.calculateVelocity(maxVelocity)
+                                    controller.onStop(
+                                        velocity =
+                                            when (orientation) {
+                                                Orientation.Horizontal -> velocity.x
+                                                Orientation.Vertical -> velocity.y
+                                            },
+                                        canChangeScene = true,
+                                    )
+                                },
+                                onDragCancel = { controller ->
+                                    controller.onStop(velocity = 0f, canChangeScene = true)
+                                },
+                                swipeDetector = swipeDetector
+                            )
+                        } catch (exception: CancellationException) {
+                            // If the coroutine scope is active, we can just restart the drag cycle.
+                            if (!isActive) {
+                                throw exception
+                            }
                         }
                     }
                 }
diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml
index 525839d..b4c839f 100644
--- a/packages/SystemUI/lint-baseline.xml
+++ b/packages/SystemUI/lint-baseline.xml
@@ -9168,39 +9168,6 @@
 
     <issue
         id="UnclosedTrace"
-        message="The `beginSection()` call is not always closed with a matching `endSection()` because the code in between may return early"
-        errorLine1="        Trace.beginSection(&quot;KeyguardViewMediator#handleKeyguardDone&quot;);"
-        errorLine2="              ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java"
-            line="2654"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="UnclosedTrace"
-        message="The `beginSection()` call is not always closed with a matching `endSection()` because the code in between may return early"
-        errorLine1="        Trace.beginSection(&quot;KeyguardViewMediator#handleShow&quot;);"
-        errorLine2="              ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java"
-            line="2780"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="UnclosedTrace"
-        message="The `beginSection()` call is not always closed with a matching `endSection()` because the code in between may return early"
-        errorLine1="        Trace.beginSection(&quot;KeyguardViewMediator#handleStartKeyguardExitAnimation&quot;);"
-        errorLine2="              ~~~~~~~~~~~~">
-        <location
-            file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java"
-            line="3011"
-            column="15"/>
-    </issue>
-
-    <issue
-        id="UnclosedTrace"
         message="The `traceBegin()` call is not always closed with a matching `traceEnd()` because the code in between may return early"
         errorLine1="            Trace.traceBegin(Trace.TRACE_TAG_APP, &quot;MediaControlPanel#bindPlayer&lt;&quot; + key + &quot;>&quot;);"
         errorLine2="                  ~~~~~~~~~~">
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt
index 722eb2b..60aea92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt
@@ -20,6 +20,9 @@
 import android.content.Context
 import android.content.Intent
 import android.content.mockedContext
+import android.os.Handler
+import android.os.fakeExecutorHandler
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -27,11 +30,13 @@
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.widgets.CommunalWidgetModule
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -41,6 +46,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -50,10 +57,13 @@
 
     @Mock private lateinit var communalInteractor: CommunalInteractor
 
-    private val mapCaptor = kotlinArgumentCaptor<Map<Int, Int>>()
+    private val mapCaptor = argumentCaptor<Map<Int, Int>>()
 
     private lateinit var context: Context
     private lateinit var broadcastDispatcher: FakeBroadcastDispatcher
+    private lateinit var secureSettings: SecureSettings
+    private lateinit var handler: Handler
+    private lateinit var fakeExecutor: FakeExecutor
     private lateinit var underTest: CommunalBackupRestoreStartable
 
     @Before
@@ -62,18 +72,28 @@
 
         context = kosmos.mockedContext
         broadcastDispatcher = kosmos.broadcastDispatcher
+        secureSettings = kosmos.fakeSettings
+        handler = kosmos.fakeExecutorHandler
+        fakeExecutor = kosmos.fakeExecutor
+
+        secureSettings.putInt(USER_SETUP_COMPLETE, 0)
 
         underTest =
             CommunalBackupRestoreStartable(
                 broadcastDispatcher,
                 communalInteractor,
                 logcatLogBuffer("CommunalBackupRestoreStartable"),
+                secureSettings,
+                handler,
             )
     }
 
     @Test
-    fun testRestoreWidgetsUponHostRestored() =
+    fun restoreWidgets_userSetUpComplete_performRestore() =
         testScope.runTest {
+            // User set up complete
+            secureSettings.putInt(USER_SETUP_COMPLETE, 1)
+
             underTest.start()
 
             // Verify restore widgets not called
@@ -94,7 +114,7 @@
 
             // Verify restore widgets called
             verify(communalInteractor).restoreWidgets(mapCaptor.capture())
-            val oldToNewWidgetIdMap = mapCaptor.value
+            val oldToNewWidgetIdMap = mapCaptor.firstValue
             assertThat(oldToNewWidgetIdMap)
                 .containsExactlyEntriesIn(
                     mapOf(
@@ -106,10 +126,54 @@
         }
 
     @Test
-    fun testDoNotRestoreWidgetsIfNotForCommunalWidgetHost() =
+    fun restoreWidgets_userSetUpNotComplete_restoreWhenUserSetupComplete() =
         testScope.runTest {
             underTest.start()
 
+            // Verify restore widgets not called
+            verify(communalInteractor, never()).restoreWidgets(any())
+
+            // Trigger app widget host restored
+            val intent =
+                Intent().apply {
+                    action = AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED
+                    putExtra(
+                        AppWidgetManager.EXTRA_HOST_ID,
+                        CommunalWidgetModule.APP_WIDGET_HOST_ID
+                    )
+                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, intArrayOf(1, 2, 3))
+                    putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(7, 8, 9))
+                }
+            broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+
+            // Verify restore widgets not called because user setup not complete
+            verify(communalInteractor, never()).restoreWidgets(any())
+
+            // User setup complete
+            secureSettings.putInt(USER_SETUP_COMPLETE, 1)
+            fakeExecutor.runAllReady()
+
+            // Verify restore widgets called
+            verify(communalInteractor).restoreWidgets(mapCaptor.capture())
+            val oldToNewWidgetIdMap = mapCaptor.firstValue
+            assertThat(oldToNewWidgetIdMap)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, 7),
+                        Pair(2, 8),
+                        Pair(3, 9),
+                    )
+                )
+        }
+
+    @Test
+    fun restoreWidgets_broadcastNotForCommunalWidgetHost_doNotPerformRestore() =
+        testScope.runTest {
+            // User set up complete
+            secureSettings.putInt(USER_SETUP_COMPLETE, 1)
+
+            underTest.start()
+
             // Trigger app widget host restored, but for another host
             val hostId = CommunalWidgetModule.APP_WIDGET_HOST_ID + 1
             val intent =
@@ -126,8 +190,11 @@
         }
 
     @Test
-    fun testAbortRestoreWidgetsIfOldToNewIdsMappingInvalid() =
+    fun restoreWidgets_oldToNewIdsMappingInvalid_abortRestore() =
         testScope.runTest {
+            // User set up complete
+            secureSettings.putInt(USER_SETUP_COMPLETE, 1)
+
             underTest.start()
 
             // Trigger app widget host restored, but new ids list is one too many for old ids
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
new file mode 100644
index 0000000..2f94933
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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.communal
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalEnabled
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@EnableFlags(FLAG_COMMUNAL_HUB)
+@RunWith(AndroidJUnit4::class)
+class CommunalOngoingContentStartableTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val mediaRepository = kosmos.fakeCommunalMediaRepository
+    private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
+    private val featureFlags =
+        kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) }
+
+    private lateinit var underTest: CommunalOngoingContentStartable
+
+    @Before
+    fun setUp() {
+        underTest =
+            CommunalOngoingContentStartable(
+                bgScope = kosmos.applicationCoroutineScope,
+                communalInteractor = kosmos.communalInteractor,
+                communalMediaRepository = mediaRepository,
+                communalSmartspaceRepository = smartspaceRepository,
+                featureFlags = featureFlags,
+            )
+    }
+
+    @Test
+    fun testListenForOngoingContentWhenCommunalIsEnabled() =
+        testScope.runTest {
+            underTest.start()
+            runCurrent()
+
+            assertThat(mediaRepository.isListening()).isFalse()
+            assertThat(smartspaceRepository.isListening()).isFalse()
+
+            kosmos.setCommunalEnabled(true)
+            runCurrent()
+
+            assertThat(mediaRepository.isListening()).isTrue()
+            assertThat(smartspaceRepository.isListening()).isTrue()
+
+            kosmos.setCommunalEnabled(false)
+            runCurrent()
+
+            assertThat(mediaRepository.isListening()).isFalse()
+            assertThat(smartspaceRepository.isListening()).isFalse()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index cf14547..0de0369 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -27,6 +27,9 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dock.dockManager
 import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -66,6 +69,7 @@
     fun setUp() {
         with(kosmos) {
             fakeSettings.putInt(Settings.System.SCREEN_OFF_TIMEOUT, SCREEN_TIMEOUT)
+            kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
 
             underTest =
                 CommunalSceneStartable(
@@ -76,6 +80,7 @@
                         keyguardInteractor = keyguardInteractor,
                         systemSettings = fakeSettings,
                         notificationShadeWindowController = notificationShadeWindowController,
+                        featureFlagsClassic = kosmos.fakeFeatureFlagsClassic,
                         applicationScope = applicationCoroutineScope,
                         bgScope = applicationCoroutineScope,
                         mainDispatcher = testDispatcher,
@@ -451,6 +456,24 @@
             }
         }
 
+    @Test
+    fun transitionFromDozingToGlanceableHub_forcesCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+                communalSceneInteractor.changeScene(CommunalScenes.Blank)
+                assertThat(scene).isEqualTo(CommunalScenes.Blank)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalScenes.Communal)
+            }
+        }
+
     private fun TestScope.updateDocked(docked: Boolean) =
         with(kosmos) {
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
new file mode 100644
index 0000000..ad2c42f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.communal.data.db
+
+import android.content.ComponentName
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason.RESTORED_FROM_BACKUP
+import com.android.systemui.communal.widgets.CommunalWidgetHost
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class DefaultWidgetPopulationTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val communalWidgetHost =
+        mock<CommunalWidgetHost> {
+            var nextId = 0
+            on { allocateIdAndBindWidget(any(), anyOrNull()) }.thenAnswer { nextId++ }
+        }
+    private val communalWidgetDao = mock<CommunalWidgetDao>()
+    private val database = mock<SupportSQLiteDatabase>()
+    private val mainUser = UserHandle(0)
+    private val userManager =
+        mock<UserManager> {
+            on { mainUser }.thenReturn(mainUser)
+            on { getUserSerialNumber(0) }.thenReturn(0)
+        }
+
+    private val defaultWidgets =
+        arrayOf(
+            "com.android.test_package_1/fake_widget_1",
+            "com.android.test_package_2/fake_widget_2",
+            "com.android.test_package_3/fake_widget_3",
+        )
+
+    private lateinit var underTest: DefaultWidgetPopulation
+
+    @Before
+    fun setUp() {
+        underTest =
+            DefaultWidgetPopulation(
+                bgScope = kosmos.applicationCoroutineScope,
+                communalWidgetHost = communalWidgetHost,
+                communalWidgetDaoProvider = { communalWidgetDao },
+                defaultWidgets = defaultWidgets,
+                logBuffer = logcatLogBuffer("DefaultWidgetPopulationTest"),
+                userManager = userManager,
+            )
+    }
+
+    @Test
+    fun testPopulateDefaultWidgetsWhenDatabaseCreated() =
+        testScope.runTest {
+            // Database created
+            underTest.onCreate(database)
+            runCurrent()
+
+            // Verify default widgets bound
+            verify(communalWidgetHost)
+                .allocateIdAndBindWidget(
+                    provider = eq(ComponentName.unflattenFromString(defaultWidgets[0])!!),
+                    user = eq(mainUser),
+                )
+            verify(communalWidgetHost)
+                .allocateIdAndBindWidget(
+                    provider = eq(ComponentName.unflattenFromString(defaultWidgets[1])!!),
+                    user = eq(mainUser),
+                )
+            verify(communalWidgetHost)
+                .allocateIdAndBindWidget(
+                    provider = eq(ComponentName.unflattenFromString(defaultWidgets[2])!!),
+                    user = eq(mainUser),
+                )
+
+            // Verify default widgets added in database
+            verify(communalWidgetDao)
+                .addWidget(
+                    widgetId = 0,
+                    componentName = defaultWidgets[0],
+                    priority = 3,
+                    userSerialNumber = 0,
+                )
+            verify(communalWidgetDao)
+                .addWidget(
+                    widgetId = 1,
+                    componentName = defaultWidgets[1],
+                    priority = 2,
+                    userSerialNumber = 0,
+                )
+            verify(communalWidgetDao)
+                .addWidget(
+                    widgetId = 2,
+                    componentName = defaultWidgets[2],
+                    priority = 1,
+                    userSerialNumber = 0,
+                )
+        }
+
+    @Test
+    fun testSkipDefaultWidgetsPopulation() =
+        testScope.runTest {
+            // Skip default widgets population
+            underTest.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP)
+
+            // Database created
+            underTest.onCreate(database)
+            runCurrent()
+
+            // Verify no widget bounded or added to the database
+            verify(communalWidgetHost, never()).allocateIdAndBindWidget(any(), any())
+            verify(communalWidgetDao, never())
+                .addWidget(
+                    widgetId = anyInt(),
+                    componentName = any(),
+                    priority = anyInt(),
+                    userSerialNumber = anyInt(),
+                )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index 407bf4c..dd28022 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -20,46 +20,41 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.util.mockito.KotlinArgumentCaptor
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @android.platform.test.annotations.EnabledOnRavenwood
 class CommunalMediaRepositoryImplTest : SysuiTestCase() {
-    @Mock private lateinit var mediaDataManager: MediaDataManager
-    @Mock private lateinit var mediaData: MediaData
-    @Mock private lateinit var tableLogBuffer: TableLogBuffer
+    private val mediaDataManager = mock<MediaDataManager>()
+    private val mediaData = mock<MediaData>()
+    private val tableLogBuffer = mock<TableLogBuffer>()
 
     private lateinit var underTest: CommunalMediaRepositoryImpl
 
-    private val mediaDataListenerCaptor: KotlinArgumentCaptor<MediaDataManager.Listener> by lazy {
-        KotlinArgumentCaptor(MediaDataManager.Listener::class.java)
-    }
+    private val mediaDataListenerCaptor = argumentCaptor<MediaDataManager.Listener>()
 
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
         underTest =
             CommunalMediaRepositoryImpl(
                 mediaDataManager,
@@ -78,6 +73,8 @@
     @Test
     fun mediaModel_updatesWhenMediaDataLoaded() =
         testScope.runTest {
+            underTest.startListening()
+
             // Listener is added
             verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
 
@@ -89,7 +86,7 @@
             // Change to media available and notify the listener.
             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
             whenever(mediaData.createdTimestampMillis).thenReturn(1234L)
-            mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
+            mediaDataListenerCaptor.firstValue.onMediaDataLoaded("key", null, mediaData)
             runCurrent()
 
             // Media active now returns true.
@@ -100,12 +97,14 @@
     @Test
     fun mediaModel_updatesWhenMediaDataRemoved() =
         testScope.runTest {
+            underTest.startListening()
+
             // Listener is added
             verify(mediaDataManager).addListener(mediaDataListenerCaptor.capture())
 
             // Change to media available and notify the listener.
             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
-            mediaDataListenerCaptor.value.onMediaDataLoaded("key", null, mediaData)
+            mediaDataListenerCaptor.firstValue.onMediaDataLoaded("key", null, mediaData)
             runCurrent()
 
             // Media active now returns true.
@@ -114,7 +113,7 @@
 
             // Change to media unavailable and notify the listener.
             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
-            mediaDataListenerCaptor.value.onMediaDataRemoved("key", false)
+            mediaDataListenerCaptor.firstValue.onMediaDataRemoved("key", false)
             runCurrent()
 
             // Media active now returns false.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt
new file mode 100644
index 0000000..c1816ed
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import android.app.smartspace.SmartspaceTarget
+import android.app.smartspace.flags.Flags.FLAG_REMOTE_VIEWS
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.smartspace.CommunalSmartspaceController
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class CommunalSmartspaceRepositoryImplTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val listenerCaptor = argumentCaptor<SmartspaceTargetListener>()
+
+    private val smartspaceController = mock<CommunalSmartspaceController>()
+    private val fakeExecutor = kosmos.fakeExecutor
+    private val systemClock = kosmos.fakeSystemClock
+
+    private lateinit var underTest: CommunalSmartspaceRepositoryImpl
+
+    @Before
+    fun setUp() {
+        underTest =
+            CommunalSmartspaceRepositoryImpl(
+                smartspaceController,
+                fakeExecutor,
+                systemClock,
+            )
+    }
+
+    @DisableFlags(FLAG_REMOTE_VIEWS)
+    @Test
+    fun startListening_remoteViewsFlagDisabled_doNotListenForSmartspaceUpdates() =
+        testScope.runTest {
+            underTest.startListening()
+            fakeExecutor.runAllReady()
+
+            verify(smartspaceController, never()).addListener(any())
+        }
+
+    @EnableFlags(FLAG_REMOTE_VIEWS)
+    @Test
+    fun startListening_remoteViewsFlagEnabled_listenForSmartspaceUpdates() =
+        testScope.runTest {
+            underTest.startListening()
+            fakeExecutor.runAllReady()
+
+            // Verify listener added
+            val listener = captureSmartspaceTargetListener()
+
+            underTest.stopListening()
+            fakeExecutor.runAllReady()
+
+            // Verify listener removed
+            verify(smartspaceController).removeListener(listener)
+        }
+
+    @EnableFlags(FLAG_REMOTE_VIEWS)
+    @Test
+    fun communalTimers_onlyShowTimersWithRemoteViews() =
+        testScope.runTest {
+            underTest.startListening()
+
+            val communalTimers by collectLastValue(underTest.timers)
+            runCurrent()
+            fakeExecutor.runAllReady()
+
+            with(captureSmartspaceTargetListener()) {
+                onSmartspaceTargetsUpdated(
+                    listOf(
+                        // Invalid. Not a timer
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("weather")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_WEATHER)
+                        },
+                        // Invalid. RemoteViews absent
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("timer-0-started")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                            on { remoteViews }.doReturn(null)
+                            on { creationTimeMillis }.doReturn(1000)
+                        },
+                        // Valid
+                        mock<SmartspaceTarget> {
+                            on { smartspaceTargetId }.doReturn("timer-1-started")
+                            on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                            on { remoteViews }.doReturn(mock())
+                            on { creationTimeMillis }.doReturn(2000)
+                        },
+                    )
+                )
+            }
+            runCurrent()
+
+            // Verify that only the valid target is listed
+            assertThat(communalTimers).hasSize(1)
+            assertThat(communalTimers?.first()?.smartspaceTargetId).isEqualTo("timer-1-started")
+        }
+
+    @EnableFlags(FLAG_REMOTE_VIEWS)
+    @Test
+    fun communalTimers_cacheCreationTime() =
+        testScope.runTest {
+            underTest.startListening()
+
+            val communalTimers by collectLastValue(underTest.timers)
+            runCurrent()
+            fakeExecutor.runAllReady()
+
+            val listener = captureSmartspaceTargetListener()
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-0-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(1000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify that the creation time is the current time, not the creation time passed in
+            // the target, because this value can be inaccurate (due to b/318535930).
+            val currentTime = systemClock.currentTimeMillis()
+            assertThat(communalTimers?.get(0)?.createdTimestampMillis).isEqualTo(currentTime)
+            assertThat(communalTimers?.get(0)?.createdTimestampMillis).isNotEqualTo(1000)
+
+            // A second timer is added.
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-0-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(2000)
+                    },
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-1-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(3000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify that the created timestamp for the first time is consistent
+            assertThat(communalTimers?.get(0)?.createdTimestampMillis).isEqualTo(currentTime)
+
+            // Verify that the second timer has a new creation time
+            assertThat(communalTimers?.get(1)?.createdTimestampMillis)
+                .isEqualTo(systemClock.currentTimeMillis())
+        }
+
+    @EnableFlags(FLAG_REMOTE_VIEWS)
+    @Test
+    fun communalTimers_creationTimeRemovedFromCacheWhenTimerRemoved() =
+        testScope.runTest {
+            underTest.startListening()
+
+            val communalTimers by collectLastValue(underTest.timers)
+            runCurrent()
+            fakeExecutor.runAllReady()
+
+            // Start timer 0
+            val listener = captureSmartspaceTargetListener()
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-0-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(1000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify timer 0 creation time
+            val expectedCreationTimeForTimer0 = systemClock.currentTimeMillis()
+            assertThat(communalTimers?.first()?.createdTimestampMillis)
+                .isEqualTo(expectedCreationTimeForTimer0)
+
+            // Advance some time
+            systemClock.advanceTime(1000)
+
+            // Start timer 1
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-0-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(1000)
+                    },
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-1-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(2000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify timer 1 creation time is new
+            val expectedCreationTimeForTimer1 = expectedCreationTimeForTimer0 + 1000
+            assertThat(communalTimers?.get(1)?.createdTimestampMillis)
+                .isEqualTo(expectedCreationTimeForTimer1)
+
+            // Removed timer 0
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-1-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(2000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify timer 0 removed, and timer 1 creation time is correct
+            assertThat(communalTimers).hasSize(1)
+            assertThat(communalTimers?.first()?.createdTimestampMillis)
+                .isEqualTo(expectedCreationTimeForTimer1)
+
+            // Advance some time
+            systemClock.advanceTime(1000)
+
+            // Start timer 0 again. Technically this is a new timer, but timers can reused stable
+            // ids.
+            listener.onSmartspaceTargetsUpdated(
+                listOf(
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-1-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(2000)
+                    },
+                    mock<SmartspaceTarget> {
+                        on { smartspaceTargetId }.doReturn("timer-0-started")
+                        on { featureType }.doReturn(SmartspaceTarget.FEATURE_TIMER)
+                        on { remoteViews }.doReturn(mock())
+                        on { creationTimeMillis }.doReturn(3000)
+                    },
+                )
+            )
+            runCurrent()
+
+            // Verify new timer added, and timer 1 creation time is still correct
+            assertThat(communalTimers).hasSize(2)
+            assertThat(communalTimers?.get(0)?.createdTimestampMillis)
+                .isEqualTo(expectedCreationTimeForTimer1)
+
+            // Verify creation time for the new timer is new, meaning that cache for timer 0 was
+            // removed when it was removed
+            assertThat(communalTimers?.get(1)?.createdTimestampMillis)
+                .isEqualTo(expectedCreationTimeForTimer1 + 1000)
+        }
+
+    @Test
+    fun stableId() {
+        assertThat(CommunalSmartspaceRepositoryImpl.stableId("timer-0-12345-started"))
+            .isEqualTo("timer-0")
+        assertThat(CommunalSmartspaceRepositoryImpl.stableId("timer-1-67890-paused"))
+            .isEqualTo("timer-1")
+        assertThat(CommunalSmartspaceRepositoryImpl.stableId("i_am_an_unexpected_id"))
+            .isEqualTo("i_am_an_unexpected_id")
+    }
+
+    private fun captureSmartspaceTargetListener(): SmartspaceTargetListener {
+        verify(smartspaceController).addListener(listenerCaptor.capture())
+        return listenerCaptor.firstValue
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 6ce6cdb..c707ebf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -24,6 +24,7 @@
 import android.content.applicationContext
 import android.graphics.Bitmap
 import android.os.UserHandle
+import android.os.userManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -33,6 +34,7 @@
 import com.android.systemui.communal.data.db.CommunalItemRank
 import com.android.systemui.communal.data.db.CommunalWidgetDao
 import com.android.systemui.communal.data.db.CommunalWidgetItem
+import com.android.systemui.communal.data.db.defaultWidgetPopulation
 import com.android.systemui.communal.nano.CommunalHubState
 import com.android.systemui.communal.proto.toByteArray
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -47,10 +49,6 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -59,11 +57,16 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -77,6 +80,9 @@
     @Mock private lateinit var communalWidgetDao: CommunalWidgetDao
     @Mock private lateinit var backupManager: BackupManager
 
+    private val communalHubStateCaptor = argumentCaptor<CommunalHubState>()
+    private val componentNameCaptor = argumentCaptor<ComponentName>()
+
     private lateinit var backupUtils: CommunalBackupUtils
     private lateinit var logBuffer: LogBuffer
     private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
@@ -85,6 +91,10 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val packageChangeRepository = kosmos.fakePackageChangeRepository
+    private val userManager = kosmos.userManager
+
+    private val mainUser = UserHandle(0)
+    private val workProfile = UserHandle(10)
 
     private val fakeAllowlist =
         listOf(
@@ -109,6 +119,9 @@
 
         whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
         whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders)
+        whenever(userManager.mainUser).thenReturn(mainUser)
+
+        restoreUser(mainUser)
 
         underTest =
             CommunalWidgetRepositoryImpl(
@@ -121,6 +134,8 @@
                 backupManager,
                 backupUtils,
                 packageChangeRepository,
+                userManager,
+                kosmos.defaultWidgetPopulation,
             )
     }
 
@@ -128,7 +143,7 @@
     fun communalWidgets_queryWidgetsFromDb() =
         testScope.runTest {
             val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
-            val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
+            val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0)
             fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
             fakeProviders.value = mapOf(1 to providerInfoA)
 
@@ -154,13 +169,13 @@
             fakeWidgets.value =
                 mapOf(
                     CommunalItemRank(uid = 1L, rank = 1) to
-                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
                     CommunalItemRank(uid = 2L, rank = 2) to
-                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
                     CommunalItemRank(uid = 3L, rank = 3) to
-                        CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L),
+                        CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0),
                     CommunalItemRank(uid = 4L, rank = 4) to
-                        CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L),
+                        CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0),
                 )
             fakeProviders.value =
                 mapOf(
@@ -192,9 +207,9 @@
             fakeWidgets.value =
                 mapOf(
                     CommunalItemRank(uid = 1L, rank = 1) to
-                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
                     CommunalItemRank(uid = 2L, rank = 2) to
-                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
                 )
             fakeProviders.value =
                 mapOf(
@@ -249,7 +264,6 @@
             val provider = ComponentName("pkg_name", "cls_name")
             val id = 1
             val priority = 1
-            val user = UserHandle(0)
             whenever(communalWidgetHost.getAppWidgetInfo(id))
                 .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
             whenever(
@@ -259,11 +273,12 @@
                     )
                 )
                 .thenReturn(id)
-            underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorSuccess)
+            underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorSuccess)
             runCurrent()
 
-            verify(communalWidgetHost).allocateIdAndBindWidget(provider, user)
-            verify(communalWidgetDao).addWidget(id, provider, priority)
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
+            verify(communalWidgetDao)
+                .addWidget(id, provider, priority, testUserSerialNumber(mainUser))
 
             // Verify backup requested
             verify(backupManager).dataChanged()
@@ -275,7 +290,6 @@
             val provider = ComponentName("pkg_name", "cls_name")
             val id = 1
             val priority = 1
-            val user = UserHandle(0)
             whenever(communalWidgetHost.getAppWidgetInfo(id))
                 .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
             whenever(
@@ -285,11 +299,12 @@
                     )
                 )
                 .thenReturn(id)
-            underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorFail)
+            underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail)
             runCurrent()
 
-            verify(communalWidgetHost).allocateIdAndBindWidget(provider, user)
-            verify(communalWidgetDao, never()).addWidget(id, provider, priority)
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
+            verify(communalWidgetDao, never())
+                .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
             verify(appWidgetHost).deleteAppWidgetId(id)
 
             // Verify backup not requested
@@ -302,7 +317,6 @@
             val provider = ComponentName("pkg_name", "cls_name")
             val id = 1
             val priority = 1
-            val user = UserHandle(0)
             whenever(communalWidgetHost.getAppWidgetInfo(id))
                 .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
             whenever(
@@ -312,13 +326,14 @@
                     )
                 )
                 .thenReturn(id)
-            underTest.addWidget(provider, user, priority) {
+            underTest.addWidget(provider, mainUser, priority) {
                 throw IllegalStateException("some error")
             }
             runCurrent()
 
-            verify(communalWidgetHost).allocateIdAndBindWidget(provider, user)
-            verify(communalWidgetDao, never()).addWidget(id, provider, priority)
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
+            verify(communalWidgetDao, never())
+                .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
             verify(appWidgetHost).deleteAppWidgetId(id)
 
             // Verify backup not requested
@@ -331,7 +346,6 @@
             val provider = ComponentName("pkg_name", "cls_name")
             val id = 1
             val priority = 1
-            val user = UserHandle(0)
             whenever(communalWidgetHost.getAppWidgetInfo(id))
                 .thenReturn(PROVIDER_INFO_CONFIGURATION_OPTIONAL)
             whenever(
@@ -341,11 +355,12 @@
                     )
                 )
                 .thenReturn(id)
-            underTest.addWidget(provider, user, priority, kosmos.widgetConfiguratorFail)
+            underTest.addWidget(provider, mainUser, priority, kosmos.widgetConfiguratorFail)
             runCurrent()
 
-            verify(communalWidgetHost).allocateIdAndBindWidget(provider, user)
-            verify(communalWidgetDao).addWidget(id, provider, priority)
+            verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
+            verify(communalWidgetDao)
+                .addWidget(id, provider, priority, testUserSerialNumber(mainUser))
 
             // Verify backup requested
             verify(backupManager).dataChanged()
@@ -444,11 +459,8 @@
             runCurrent()
 
             // Verify state restored, and widget 2 skipped
-            val restoredState =
-                withArgCaptor<CommunalHubState> {
-                    verify(communalWidgetDao).restoreCommunalHubState(capture())
-                }
-            val restoredWidgets = restoredState.widgets.toList()
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
             assertThat(restoredWidgets).hasSize(1)
 
             val restoredWidget = restoredWidgets.first()
@@ -474,11 +486,8 @@
             runCurrent()
 
             // Verify widget 1 and 2 are restored, and are now 11 and 12.
-            val restoredState =
-                withArgCaptor<CommunalHubState> {
-                    verify(communalWidgetDao).restoreCommunalHubState(capture())
-                }
-            val restoredWidgets = restoredState.widgets.toList()
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
             assertThat(restoredWidgets).hasSize(2)
 
             val restoredWidget1 = restoredWidgets[0]
@@ -512,11 +521,8 @@
             runCurrent()
 
             // Verify widget 1 and 2 are restored, and are now 1 and 12.
-            val restoredState =
-                withArgCaptor<CommunalHubState> {
-                    verify(communalWidgetDao).restoreCommunalHubState(capture())
-                }
-            val restoredWidgets = restoredState.widgets.toList()
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
             assertThat(restoredWidgets).hasSize(2)
 
             val restoredWidget1 = restoredWidgets[0]
@@ -533,14 +539,134 @@
         }
 
     @Test
+    fun restoreWidgets_undefinedUser_restoredAsMain() =
+        testScope.runTest {
+            // Write two widgets to file, both of which have user serial number undefined.
+            val fakeState =
+                CommunalHubState().apply {
+                    widgets =
+                        listOf(
+                                CommunalHubState.CommunalWidgetItem().apply {
+                                    widgetId = 1
+                                    componentName = "pk_name/fake_widget_1"
+                                    rank = 1
+                                    userSerialNumber =
+                                        CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED
+                                },
+                                CommunalHubState.CommunalWidgetItem().apply {
+                                    widgetId = 2
+                                    componentName = "pk_name/fake_widget_2"
+                                    rank = 2
+                                    userSerialNumber =
+                                        CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED
+                                },
+                            )
+                            .toTypedArray()
+                }
+            backupUtils.writeBytesToDisk(fakeState.toByteArray())
+
+            // Set up app widget host with widget ids.
+            setAppWidgetIds(listOf(11, 12))
+
+            // Restore widgets.
+            underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12)))
+            runCurrent()
+
+            // Verify widget 1 and 2 are restored with the main user.
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
+            assertThat(restoredWidgets).hasSize(2)
+
+            val restoredWidget1 = restoredWidgets[0]
+            assertThat(restoredWidget1.widgetId).isEqualTo(11)
+            assertThat(restoredWidget1.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser))
+
+            val restoredWidget2 = restoredWidgets[1]
+            assertThat(restoredWidget2.widgetId).isEqualTo(12)
+            assertThat(restoredWidget2.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser))
+        }
+
+    @Test
+    fun restoreWidgets_workProfileNotRestored_widgetSkipped() =
+        testScope.runTest {
+            // Write fake state to file
+            backupUtils.writeBytesToDisk(fakeStateWithWorkProfile.toByteArray())
+
+            // Set up app widget host with widget ids.
+            // (b/349852237) It's possible that the platform restores widgets even though their user
+            // is not restored.
+            setAppWidgetIds(listOf(11, 12))
+
+            // Restore widgets.
+            underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12)))
+            runCurrent()
+
+            // Verify only widget 1 is restored. Widget 2 is skipped because it belongs to a work
+            // profile, which is not restored.
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
+            assertThat(restoredWidgets).hasSize(1)
+
+            val restoredWidget = restoredWidgets[0]
+            assertThat(restoredWidget.widgetId).isEqualTo(11)
+            assertThat(restoredWidget.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser))
+        }
+
+    @Test
+    fun restoreWidgets_workProfileRestored_manuallyBindWidget() =
+        testScope.runTest {
+            // Write fake state to file
+            backupUtils.writeBytesToDisk(fakeStateWithWorkProfile.toByteArray())
+
+            // Set up app widget host with widget ids.
+            // (b/349852237) It's possible that the platform restores widgets even though their user
+            // is not restored.
+            setAppWidgetIds(listOf(11, 12))
+
+            // Restore work profile.
+            restoreUser(workProfile)
+
+            val newWidgetId = 13
+            whenever(communalWidgetHost.allocateIdAndBindWidget(any(), any()))
+                .thenReturn(newWidgetId)
+
+            // Restore widgets.
+            underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12)))
+            runCurrent()
+
+            // Verify widget 1 is restored.
+            verify(communalWidgetDao).restoreCommunalHubState(communalHubStateCaptor.capture())
+            val restoredWidgets = communalHubStateCaptor.firstValue.widgets.toList()
+            assertThat(restoredWidgets).hasSize(1)
+
+            val restoredWidget = restoredWidgets[0]
+            assertThat(restoredWidget.widgetId).isEqualTo(11)
+            assertThat(restoredWidget.userSerialNumber).isEqualTo(testUserSerialNumber(mainUser))
+
+            // Verify widget 2 (now 12) is removed from platform
+            verify(appWidgetHost).deleteAppWidgetId(12)
+
+            // Verify work profile widget is manually bound
+            verify(communalWidgetDao)
+                .addWidget(
+                    eq(newWidgetId),
+                    componentNameCaptor.capture(),
+                    eq(2),
+                    eq(testUserSerialNumber(workProfile))
+                )
+            assertThat(componentNameCaptor.firstValue)
+                .isEqualTo(ComponentName("pk_name", "fake_widget_2"))
+        }
+
+    @Test
     fun pendingWidgets() =
         testScope.runTest {
             fakeWidgets.value =
                 mapOf(
                     CommunalItemRank(uid = 1L, rank = 1) to
-                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
                     CommunalItemRank(uid = 2L, rank = 2) to
-                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
                 )
 
             // Widget 1 is installed
@@ -554,7 +680,7 @@
                         sessionId = 1,
                         packageName = "pk_2",
                         icon = fakeIcon,
-                        user = UserHandle.CURRENT,
+                        user = mainUser,
                     )
                 )
             )
@@ -572,7 +698,7 @@
                         priority = 2,
                         packageName = "pk_2",
                         icon = fakeIcon,
-                        user = UserHandle.CURRENT,
+                        user = mainUser,
                     ),
                 )
         }
@@ -583,7 +709,7 @@
             fakeWidgets.value =
                 mapOf(
                     CommunalItemRank(uid = 1L, rank = 1) to
-                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
                 )
 
             // Widget 1 is pending install
@@ -594,7 +720,7 @@
                         sessionId = 1,
                         packageName = "pk_1",
                         icon = fakeIcon,
-                        user = UserHandle.CURRENT,
+                        user = mainUser,
                     )
                 )
             )
@@ -607,7 +733,7 @@
                         priority = 1,
                         packageName = "pk_1",
                         icon = fakeIcon,
-                        user = UserHandle.CURRENT,
+                        user = mainUser,
                     ),
                 )
 
@@ -633,6 +759,20 @@
         whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
     }
 
+    // Commonly the user id and user serial number are the same, but for testing purposes use a
+    // simple algorithm to map a user id to a different user serial number to make sure the correct
+    // value is used.
+    private fun testUserSerialNumber(user: UserHandle): Int {
+        return user.identifier + 100
+    }
+
+    private fun restoreUser(user: UserHandle) {
+        whenever(backupManager.getUserForAncestralSerialNumber(user.identifier.toLong()))
+            .thenReturn(user)
+        whenever(userManager.getUserSerialNumber(user.identifier))
+            .thenReturn(testUserSerialNumber(user))
+    }
+
     private companion object {
         val PROVIDER_INFO_REQUIRES_CONFIGURATION =
             AppWidgetProviderInfo().apply { configure = ComponentName("test.pkg", "test.cmp") }
@@ -650,11 +790,32 @@
                                 widgetId = 1
                                 componentName = "pk_name/fake_widget_1"
                                 rank = 1
+                                userSerialNumber = 0
                             },
                             CommunalHubState.CommunalWidgetItem().apply {
                                 widgetId = 2
                                 componentName = "pk_name/fake_widget_2"
                                 rank = 2
+                                userSerialNumber = 0
+                            },
+                        )
+                        .toTypedArray()
+            }
+        val fakeStateWithWorkProfile =
+            CommunalHubState().apply {
+                widgets =
+                    listOf(
+                            CommunalHubState.CommunalWidgetItem().apply {
+                                widgetId = 1
+                                componentName = "pk_name/fake_widget_1"
+                                rank = 1
+                                userSerialNumber = 0
+                            },
+                            CommunalHubState.CommunalWidgetItem().apply {
+                                widgetId = 2
+                                componentName = "pk_name/fake_widget_2"
+                                rank = 2
+                                userSerialNumber = 10
                             },
                         )
                         .toTypedArray()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 7b26db5..5cdbe9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -19,7 +19,6 @@
 
 import android.app.admin.DevicePolicyManager
 import android.app.admin.devicePolicyManager
-import android.app.smartspace.SmartspaceTarget
 import android.appwidget.AppWidgetProviderInfo
 import android.content.Intent
 import android.content.pm.UserInfo
@@ -36,14 +35,17 @@
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -69,8 +71,6 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.fakeUserTracker
-import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
-import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
@@ -114,7 +114,7 @@
     private lateinit var communalRepository: FakeCommunalSceneRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
-    private lateinit var smartspaceRepository: FakeSmartspaceRepository
+    private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository
     private lateinit var userRepository: FakeUserRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
@@ -135,7 +135,7 @@
         communalRepository = kosmos.fakeCommunalSceneRepository
         mediaRepository = kosmos.fakeCommunalMediaRepository
         widgetRepository = kosmos.fakeCommunalWidgetRepository
-        smartspaceRepository = kosmos.fakeSmartspaceRepository
+        smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
         userRepository = kosmos.fakeUserRepository
         keyguardRepository = kosmos.fakeKeyguardRepository
         editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter
@@ -265,44 +265,6 @@
         }
 
     @Test
-    fun smartspace_onlyShowTimersWithRemoteViews() =
-        testScope.runTest {
-            // Keyguard showing, and tutorial completed.
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardRepository.setKeyguardOccluded(false)
-            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
-
-            // Not a timer
-            val target1 = mock(SmartspaceTarget::class.java)
-            whenever(target1.smartspaceTargetId).thenReturn("target1")
-            whenever(target1.featureType).thenReturn(SmartspaceTarget.FEATURE_WEATHER)
-            whenever(target1.remoteViews).thenReturn(mock(RemoteViews::class.java))
-            whenever(target1.creationTimeMillis).thenReturn(0L)
-
-            // Does not have RemoteViews
-            val target2 = mock(SmartspaceTarget::class.java)
-            whenever(target2.smartspaceTargetId).thenReturn("target2")
-            whenever(target2.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target2.remoteViews).thenReturn(null)
-            whenever(target2.creationTimeMillis).thenReturn(0L)
-
-            // Timer and has RemoteViews
-            val target3 = mock(SmartspaceTarget::class.java)
-            whenever(target3.smartspaceTargetId).thenReturn("target3")
-            whenever(target3.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target3.remoteViews).thenReturn(mock(RemoteViews::class.java))
-            whenever(target3.creationTimeMillis).thenReturn(0L)
-
-            val targets = listOf(target1, target2, target3)
-            smartspaceRepository.setCommunalSmartspaceTargets(targets)
-
-            val smartspaceContent by collectLastValue(underTest.getOngoingContent(true))
-            assertThat(smartspaceContent?.size).isEqualTo(1)
-            assertThat(smartspaceContent?.get(0)?.key)
-                .isEqualTo(CommunalContentModel.KEY.smartspace("target3"))
-        }
-
-    @Test
     fun smartspaceDynamicSizing_oneCard_fullSize() =
         testSmartspaceDynamicSizing(
             totalTargets = 1,
@@ -387,12 +349,12 @@
             keyguardRepository.setKeyguardOccluded(false)
             tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
 
-            val targets = mutableListOf<SmartspaceTarget>()
+            val targets = mutableListOf<CommunalSmartspaceTimer>()
             for (index in 0 until totalTargets) {
                 targets.add(smartspaceTimer(index.toString()))
             }
 
-            smartspaceRepository.setCommunalSmartspaceTargets(targets)
+            smartspaceRepository.setTimers(targets)
 
             val smartspaceContent by collectLastValue(underTest.getOngoingContent(true))
             assertThat(smartspaceContent?.size).isEqualTo(totalTargets)
@@ -441,18 +403,18 @@
 
             // Timer1 started
             val timer1 = smartspaceTimer("timer1", timestamp = 1L)
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1))
+            smartspaceRepository.setTimers(listOf(timer1))
 
             // Umo started
             mediaRepository.mediaActive(timestamp = 2L)
 
             // Timer2 started
             val timer2 = smartspaceTimer("timer2", timestamp = 3L)
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2))
+            smartspaceRepository.setTimers(listOf(timer1, timer2))
 
             // Timer3 started
             val timer3 = smartspaceTimer("timer3", timestamp = 4L)
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(timer1, timer2, timer3))
+            smartspaceRepository.setTimers(listOf(timer1, timer2, timer3))
 
             val ongoingContent by collectLastValue(underTest.getOngoingContent(true))
             assertThat(ongoingContent?.size).isEqualTo(4)
@@ -1089,13 +1051,12 @@
             assertThat(showCommunalFromOccluded).isTrue()
         }
 
-    private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
-        val timer = mock(SmartspaceTarget::class.java)
-        whenever(timer.smartspaceTargetId).thenReturn(id)
-        whenever(timer.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-        whenever(timer.remoteViews).thenReturn(mock(RemoteViews::class.java))
-        whenever(timer.creationTimeMillis).thenReturn(timestamp)
-        return timer
+    private fun smartspaceTimer(id: String, timestamp: Long = 0L): CommunalSmartspaceTimer {
+        return CommunalSmartspaceTimer(
+            smartspaceTargetId = id,
+            createdTimestampMillis = timestamp,
+            remoteViews = mock(RemoteViews::class.java)
+        )
     }
 
     private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 0190ccb..b138fb3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.communal.view.viewmodel
 
-import android.app.smartspace.SmartspaceTarget
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ActivityNotFoundException
 import android.content.Intent
@@ -32,12 +31,16 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalPrefsInteractor
@@ -57,8 +60,6 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.settings.fakeUserTracker
-import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
-import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.any
@@ -76,6 +77,8 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -91,9 +94,10 @@
 
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
-    private lateinit var smartspaceRepository: FakeSmartspaceRepository
+    private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var communalSceneInteractor: CommunalSceneInteractor
+    private lateinit var communalInteractor: CommunalInteractor
 
     private val testableResources = context.orCreateTestableResources
 
@@ -105,9 +109,10 @@
 
         tutorialRepository = kosmos.fakeCommunalTutorialRepository
         widgetRepository = kosmos.fakeCommunalWidgetRepository
-        smartspaceRepository = kosmos.fakeSmartspaceRepository
+        smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
         mediaRepository = kosmos.fakeCommunalMediaRepository
         communalSceneInteractor = kosmos.communalSceneInteractor
+        communalInteractor = spy(kosmos.communalInteractor)
         kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
         kosmos.fakeUserTracker.set(
             userInfos = listOf(MAIN_USER_INFO),
@@ -119,7 +124,7 @@
         underTest =
             CommunalEditModeViewModel(
                 communalSceneInteractor,
-                kosmos.communalInteractor,
+                communalInteractor,
                 kosmos.communalSettingsInteractor,
                 kosmos.keyguardTransitionInteractor,
                 mediaHost,
@@ -152,11 +157,15 @@
             widgetRepository.setCommunalWidgets(widgets)
 
             // Smartspace available.
-            val target = Mockito.mock(SmartspaceTarget::class.java)
-            whenever(target.smartspaceTargetId).thenReturn("target")
-            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            smartspaceRepository.setTimers(
+                listOf(
+                    CommunalSmartspaceTimer(
+                        smartspaceTargetId = "target",
+                        createdTimestampMillis = 0L,
+                        remoteViews = Mockito.mock(RemoteViews::class.java),
+                    )
+                )
+            )
 
             // Media playing.
             mediaRepository.mediaActive()
@@ -342,6 +351,16 @@
             assertThat(showDisclaimer).isFalse()
         }
 
+    @Test
+    fun scrollPosition_persistedOnEditCleanup() {
+        val index = 2
+        val offset = 30
+        underTest.onScrollPositionUpdated(index, offset)
+        underTest.cleanupEditModeState()
+
+        verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
+    }
+
     private companion object {
         val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
         const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index d338774..c480aa8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.communal.view.viewmodel
 
-import android.app.smartspace.SmartspaceTarget
 import android.appwidget.AppWidgetProviderInfo
 import android.content.pm.UserInfo
 import android.os.UserHandle
@@ -27,14 +26,18 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
@@ -78,8 +81,6 @@
 import com.android.systemui.shade.ShadeTestUtil
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.shadeTestUtil
-import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
-import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
@@ -97,7 +98,9 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
@@ -115,12 +118,13 @@
     private lateinit var keyguardRepository: FakeKeyguardRepository
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
-    private lateinit var smartspaceRepository: FakeSmartspaceRepository
+    private lateinit var smartspaceRepository: FakeCommunalSmartspaceRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var userRepository: FakeUserRepository
     private lateinit var shadeTestUtil: ShadeTestUtil
     private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
     private lateinit var communalRepository: FakeCommunalSceneRepository
+    private lateinit var communalInteractor: CommunalInteractor
 
     private lateinit var underTest: CommunalViewModel
 
@@ -136,7 +140,7 @@
         keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
         tutorialRepository = kosmos.fakeCommunalTutorialRepository
         widgetRepository = kosmos.fakeCommunalWidgetRepository
-        smartspaceRepository = kosmos.fakeSmartspaceRepository
+        smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
         mediaRepository = kosmos.fakeCommunalMediaRepository
         userRepository = kosmos.fakeUserRepository
         shadeTestUtil = kosmos.shadeTestUtil
@@ -154,6 +158,8 @@
 
         kosmos.powerInteractor.setAwakeForTest()
 
+        communalInteractor = spy(kosmos.communalInteractor)
+
         underTest =
             CommunalViewModel(
                 kosmos.testDispatcher,
@@ -164,7 +170,7 @@
                 kosmos.keyguardInteractor,
                 mock<KeyguardIndicationController>(),
                 kosmos.communalSceneInteractor,
-                kosmos.communalInteractor,
+                communalInteractor,
                 kosmos.communalSettingsInteractor,
                 kosmos.communalTutorialInteractor,
                 kosmos.shadeInteractor,
@@ -222,11 +228,15 @@
             widgetRepository.setCommunalWidgets(widgets)
 
             // Smartspace available.
-            val target = Mockito.mock(SmartspaceTarget::class.java)
-            whenever(target.smartspaceTargetId).thenReturn("target")
-            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            smartspaceRepository.setTimers(
+                listOf(
+                    CommunalSmartspaceTimer(
+                        smartspaceTargetId = "target",
+                        createdTimestampMillis = 0L,
+                        remoteViews = Mockito.mock(RemoteViews::class.java),
+                    )
+                )
+            )
 
             // Media playing.
             mediaRepository.mediaActive()
@@ -293,7 +303,7 @@
             widgetRepository.setCommunalWidgets(emptyList())
             // UMO playing
             mediaRepository.mediaActive()
-            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())
+            smartspaceRepository.setTimers(emptyList())
 
             val isEmptyState by collectLastValue(underTest.isEmptyState)
             assertThat(isEmptyState).isTrue()
@@ -314,7 +324,7 @@
                 ),
             )
             mediaRepository.mediaInactive()
-            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())
+            smartspaceRepository.setTimers(emptyList())
 
             val isEmptyState by collectLastValue(underTest.isEmptyState)
             assertThat(isEmptyState).isFalse()
@@ -689,11 +699,15 @@
             advanceTimeBy(60L)
 
             // New timer available
-            val target = Mockito.mock(SmartspaceTarget::class.java)
-            whenever<String?>(target.smartspaceTargetId).thenReturn("target")
-            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            smartspaceRepository.setTimers(
+                listOf(
+                    CommunalSmartspaceTimer(
+                        smartspaceTargetId = "target",
+                        createdTimestampMillis = 0L,
+                        remoteViews = Mockito.mock(RemoteViews::class.java),
+                    )
+                )
+            )
             runCurrent()
 
             // Still only emits widgets and the CTA tile
@@ -748,11 +762,15 @@
             assertThat(communalContent).hasSize(3)
 
             // When new timer available
-            val target = Mockito.mock(SmartspaceTarget::class.java)
-            whenever(target.smartspaceTargetId).thenReturn("target")
-            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
-            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
-            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            smartspaceRepository.setTimers(
+                listOf(
+                    CommunalSmartspaceTimer(
+                        smartspaceTargetId = "target",
+                        createdTimestampMillis = 0L,
+                        remoteViews = Mockito.mock(RemoteViews::class.java),
+                    )
+                )
+            )
             runCurrent()
 
             // Then emits timer, widgets and the CTA tile
@@ -767,6 +785,16 @@
                 .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
         }
 
+    @Test
+    fun scrollPosition_persistedOnEditEntry() {
+        val index = 2
+        val offset = 30
+        underTest.onScrollPositionUpdated(index, offset)
+        underTest.onOpenWidgetEditor(false)
+
+        verify(communalInteractor).setScrollPosition(eq(index), eq(offset))
+    }
+
     private suspend fun setIsMainUser(isMainUser: Boolean) {
         val user = if (isMainUser) MAIN_USER_INFO else SECONDARY_USER_INFO
         with(userRepository) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 5a39de8..444f63a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -34,6 +34,9 @@
 import androidx.lifecycle.LifecycleRegistry
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.app.viewcapture.ViewCaptureFactory
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -79,6 +82,7 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.isNull
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -114,11 +118,11 @@
 
     @Mock
     lateinit var mDreamComplicationComponentFactory:
-        com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
+            com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
 
     @Mock
     lateinit var mDreamComplicationComponent:
-        com.android.systemui.dreams.complication.dagger.ComplicationComponent
+            com.android.systemui.dreams.complication.dagger.ComplicationComponent
 
     @Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler
 
@@ -154,8 +158,12 @@
 
     @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController
 
+    @Mock lateinit var mLazyViewCapture: Lazy<ViewCapture>
+
+    private lateinit var mViewCaptureAwareWindowManager: ViewCaptureAwareWindowManager
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
     private lateinit var communalRepository: FakeCommunalSceneRepository
+    private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context))
 
     @Captor var mViewCaptor: ArgumentCaptor<View>? = null
     private lateinit var mService: DreamOverlayService
@@ -192,13 +200,16 @@
         whenever(mDreamOverlayContainerViewController.containerView)
             .thenReturn(mDreamOverlayContainerView)
         whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
+        whenever(mLazyViewCapture.value).thenReturn(viewCaptureSpy)
         mWindowParams = WindowManager.LayoutParams()
+        mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mWindowManager,
+                mLazyViewCapture, isViewCaptureEnabled = false)
         mService =
             DreamOverlayService(
                 mContext,
                 mLifecycleOwner,
                 mMainExecutor,
-                mWindowManager,
+                mViewCaptureAwareWindowManager,
                 mComplicationComponentFactory,
                 mDreamComplicationComponentFactory,
                 mDreamOverlayComponentFactory,
@@ -246,7 +257,7 @@
         mMainExecutor.runAllReady()
         verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
         verify(mUiEventLogger)
-            .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
+                .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 612f2e7..ec4fd79 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -34,12 +34,14 @@
 
 import android.os.PowerManager
 import android.platform.test.annotations.EnableFlags
+import android.service.dream.dreamManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -64,8 +66,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -120,6 +124,66 @@
 
     @Test
     @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+            runCurrent()
+
+            // If dreaming is possible and communal is available, then we should transition to
+            // GLANCEABLE_HUB when waking up due to power button press.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onPowerButtonPress_canNotDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+            runCurrent()
+
+            // If dreaming is NOT possible but communal is available, then we should transition to
+            // LOCKSCREEN when waking up due to power button press.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onPowerButtonPress_canNDream_glanceableHubNotAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(false)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_POWER_BUTTON)
+            runCurrent()
+
+            // If dreaming is possible but communal is NOT available, then we should transition to
+            // LOCKSCREEN when waking up due to power button press.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
         testScope.runTest {
             kosmos.fakeCommunalSceneRepository.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
new file mode 100644
index 0000000..583c10f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.app.Flags
+import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
+import android.provider.Settings.Global.ZEN_MODE_OFF
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.data.repository.FakeZenModeRepository
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesTileDataInteractorTest : SysuiTestCase() {
+    private val zenModeRepository = FakeZenModeRepository()
+
+    private val underTest = ModesTileDataInteractor(zenModeRepository)
+
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    @Test
+    fun availableWhenFlagIsOn() = runTest {
+        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+
+        assertThat(availability).containsExactly(true)
+    }
+
+    @DisableFlags(Flags.FLAG_MODES_UI)
+    @Test
+    fun unavailableWhenFlagIsOff() = runTest {
+        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+
+        assertThat(availability).containsExactly(false)
+    }
+
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    @Test
+    fun dataMatchesTheRepository() = runTest {
+        val dataList: List<ModesTileModel> by
+            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
+        runCurrent()
+
+        // Enable zen mode
+        zenModeRepository.updateZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+        runCurrent()
+
+        // Change zen mode: it's still enabled, so this shouldn't cause another emission
+        zenModeRepository.updateZenMode(ZEN_MODE_NO_INTERRUPTIONS)
+        runCurrent()
+
+        // Disable zen mode
+        zenModeRepository.updateZenMode(ZEN_MODE_OFF)
+        runCurrent()
+
+        assertThat(dataList.map { it.isActivated }).containsExactly(false, true, false)
+    }
+
+    private companion object {
+
+        val TEST_USER = UserHandle.of(1)!!
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index b5e47d1..fd1b213 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -85,7 +85,6 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -182,7 +181,6 @@
 
             kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
                 buildNotificationRows(isPinned = false)
-            advanceTimeBy(50L) // account for HeadsUpNotificationInteractor debounce
             assertThat(isVisible).isFalse()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 5ef3485..8b4265f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -23,7 +23,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -280,64 +279,6 @@
         }
 
     @Test
-    fun isHeadsUpOrAnimatingAway_falseOnStart() =
-        testScope.runTest {
-            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
-
-            runCurrent()
-
-            assertThat(isHeadsUpOrAnimatingAway).isFalse()
-        }
-
-    @Test
-    fun isHeadsUpOrAnimatingAway_hasPinnedRows() =
-        testScope.runTest {
-            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
-
-            // WHEN a row is pinned
-            headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
-            runCurrent()
-
-            assertThat(isHeadsUpOrAnimatingAway).isTrue()
-        }
-
-    @Test
-    fun isHeadsUpOrAnimatingAway_headsUpAnimatingAway() =
-        testScope.runTest {
-            val isHeadsUpOrAnimatingAway by collectLastValue(underTest.isHeadsUpOrAnimatingAway)
-
-            // WHEN the last row is animating away
-            headsUpRepository.setHeadsUpAnimatingAway(true)
-            runCurrent()
-
-            assertThat(isHeadsUpOrAnimatingAway).isTrue()
-        }
-
-    @Test
-    fun isHeadsUpOrAnimatingAway_headsUpAnimatingAwayDebounced() =
-        testScope.runTest {
-            val values by collectValues(underTest.isHeadsUpOrAnimatingAway)
-
-            // GIVEN a row is pinned
-            headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true))
-            runCurrent()
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values.first()).isFalse() // initial value
-            assertThat(values.last()).isTrue()
-
-            // WHEN the last row is removed
-            headsUpRepository.setNotifications(emptyList())
-            runCurrent()
-            // AND starts to animate away
-            headsUpRepository.setHeadsUpAnimatingAway(true)
-            runCurrent()
-
-            // THEN isHeadsUpOrAnimatingAway remained true
-            assertThat(values.size).isEqualTo(2)
-            assertThat(values.last()).isTrue()
-        }
-
-    @Test
     fun showHeadsUpStatusBar_true() =
         testScope.runTest {
             val showHeadsUpStatusBar by collectLastValue(underTest.showHeadsUpStatusBar)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
deleted file mode 100644
index 3d3438e..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2018 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.policy;
-
-import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
-import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
-
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.os.Handler;
-import android.platform.test.flag.junit.FlagsParameterization;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.res.R;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
-import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
-import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.kotlin.JavaAdapter;
-import com.android.systemui.util.settings.GlobalSettings;
-import com.android.systemui.util.time.SystemClock;
-
-import kotlinx.coroutines.flow.StateFlowKt;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
-import java.util.List;
-
-@SmallTest
-@RunWith(ParameterizedAndroidJunit4.class)
-@TestableLooper.RunWithLooper
-public class HeadsUpManagerPhoneTest extends BaseHeadsUpManagerTest {
-    @Rule public MockitoRule rule = MockitoJUnit.rule();
-
-    private final HeadsUpManagerLogger mHeadsUpManagerLogger = new HeadsUpManagerLogger(
-            logcatLogBuffer());
-    @Mock private GroupMembershipManager mGroupManager;
-    @Mock private VisualStabilityProvider mVSProvider;
-    @Mock private StatusBarStateController mStatusBarStateController;
-    @Mock private KeyguardBypassController mBypassController;
-    @Mock private ConfigurationControllerImpl mConfigurationController;
-    @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper;
-    @Mock private UiEventLogger mUiEventLogger;
-    @Mock private JavaAdapter mJavaAdapter;
-    @Mock private ShadeInteractor mShadeInteractor;
-    @Mock private DumpManager dumpManager;
-    private AvalancheController mAvalancheController;
-
-    @Mock private Handler mBgHandler;
-
-    private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
-        TestableHeadsUpManagerPhone(
-                Context context,
-                HeadsUpManagerLogger headsUpManagerLogger,
-                GroupMembershipManager groupManager,
-                VisualStabilityProvider visualStabilityProvider,
-                StatusBarStateController statusBarStateController,
-                KeyguardBypassController keyguardBypassController,
-                ConfigurationController configurationController,
-                GlobalSettings globalSettings,
-                SystemClock systemClock,
-                DelayableExecutor executor,
-                AccessibilityManagerWrapper accessibilityManagerWrapper,
-                UiEventLogger uiEventLogger,
-                JavaAdapter javaAdapter,
-                ShadeInteractor shadeInteractor,
-                AvalancheController avalancheController
-        ) {
-            super(
-                    context,
-                    headsUpManagerLogger,
-                    statusBarStateController,
-                    keyguardBypassController,
-                    groupManager,
-                    visualStabilityProvider,
-                    configurationController,
-                    mockExecutorHandler(executor),
-                    globalSettings,
-                    systemClock,
-                    executor,
-                    accessibilityManagerWrapper,
-                    uiEventLogger,
-                    javaAdapter,
-                    shadeInteractor,
-                    avalancheController
-            );
-            mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
-            mAutoDismissTime = TEST_AUTO_DISMISS_TIME;
-        }
-    }
-
-    private HeadsUpManagerPhone createHeadsUpManagerPhone() {
-        return new TestableHeadsUpManagerPhone(
-                mContext,
-                mHeadsUpManagerLogger,
-                mGroupManager,
-                mVSProvider,
-                mStatusBarStateController,
-                mBypassController,
-                mConfigurationController,
-                mGlobalSettings,
-                mSystemClock,
-                mExecutor,
-                mAccessibilityManagerWrapper,
-                mUiEventLogger,
-                mJavaAdapter,
-                mShadeInteractor,
-                mAvalancheController
-        );
-    }
-
-    @Parameters(name = "{0}")
-    public static List<FlagsParameterization> getFlags() {
-        return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME);
-    }
-
-    public HeadsUpManagerPhoneTest(FlagsParameterization flags) {
-        super(flags);
-    }
-
-    @Before
-    public void setUp() {
-        when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false));
-        final AccessibilityManagerWrapper accessibilityMgr =
-                mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
-        when(accessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt()))
-                .thenReturn(TEST_AUTO_DISMISS_TIME);
-        when(mVSProvider.isReorderingAllowed()).thenReturn(true);
-        mDependency.injectMockDependency(NotificationShadeWindowController.class);
-        mContext.getOrCreateTestableResources().addOverride(
-                R.integer.ambient_notification_extension_time, 500);
-
-        mAvalancheController = new AvalancheController(dumpManager, mUiEventLogger, mBgHandler);
-    }
-
-    @Test
-    public void testSnooze() {
-        final HeadsUpManager hmp = createHeadsUpManagerPhone();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        hmp.showNotification(entry);
-        hmp.snooze();
-
-        assertTrue(hmp.isSnoozed(entry.getSbn().getPackageName()));
-    }
-
-    @Test
-    public void testSwipedOutNotification() {
-        final HeadsUpManager hmp = createHeadsUpManagerPhone();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        hmp.showNotification(entry);
-        hmp.addSwipedOutNotification(entry.getKey());
-
-        // Remove should succeed because the notification is swiped out
-        final boolean removedImmediately = hmp.removeNotification(entry.getKey(),
-                /* releaseImmediately = */ false);
-
-        assertTrue(removedImmediately);
-        assertFalse(hmp.isHeadsUpEntry(entry.getKey()));
-    }
-
-    @Test
-    public void testCanRemoveImmediately_swipedOut() {
-        final HeadsUpManager hmp = createHeadsUpManagerPhone();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        hmp.showNotification(entry);
-        hmp.addSwipedOutNotification(entry.getKey());
-
-        // Notification is swiped so it can be immediately removed.
-        assertTrue(hmp.canRemoveImmediately(entry.getKey()));
-    }
-
-    @Ignore("b/141538055")
-    @Test
-    public void testCanRemoveImmediately_notTopEntry() {
-        final HeadsUpManager hmp = createHeadsUpManagerPhone();
-        final NotificationEntry earlierEntry =
-                HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        final NotificationEntry laterEntry =
-                HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext);
-        laterEntry.setRow(mRow);
-
-        hmp.showNotification(earlierEntry);
-        hmp.showNotification(laterEntry);
-
-        // Notification is "behind" a higher priority notification so we can remove it immediately.
-        assertTrue(hmp.canRemoveImmediately(earlierEntry.getKey()));
-    }
-
-    @Test
-    public void testExtendHeadsUp() {
-        final HeadsUpManagerPhone hmp = createHeadsUpManagerPhone();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        hmp.showNotification(entry);
-        hmp.extendHeadsUp();
-        mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2);
-
-        assertTrue(hmp.isHeadsUpEntry(entry.getKey()));
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
new file mode 100644
index 0000000..663cf1c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2018 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.policy
+
+import android.content.Context
+import android.os.Handler
+import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
+import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.mockExecutorHandler
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.time.SystemClock
+import junit.framework.Assert
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+@RunWithLooper
+class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManagerTest(flags) {
+
+    private val mHeadsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
+
+    @Mock private lateinit var mGroupManager: GroupMembershipManager
+
+    @Mock private lateinit var mVSProvider: VisualStabilityProvider
+
+    @Mock private lateinit var mStatusBarStateController: StatusBarStateController
+
+    @Mock private lateinit var mBypassController: KeyguardBypassController
+
+    @Mock private lateinit var mConfigurationController: ConfigurationControllerImpl
+
+    @Mock private lateinit var mAccessibilityManagerWrapper: AccessibilityManagerWrapper
+
+    @Mock private lateinit var mUiEventLogger: UiEventLogger
+
+    @Mock private lateinit var mJavaAdapter: JavaAdapter
+
+    @Mock private lateinit var mShadeInteractor: ShadeInteractor
+
+    @Mock private lateinit var dumpManager: DumpManager
+    private lateinit var mAvalancheController: AvalancheController
+
+    @Mock private lateinit var mBgHandler: Handler
+
+    private class TestableHeadsUpManagerPhone(
+        context: Context,
+        headsUpManagerLogger: HeadsUpManagerLogger,
+        groupManager: GroupMembershipManager,
+        visualStabilityProvider: VisualStabilityProvider,
+        statusBarStateController: StatusBarStateController,
+        keyguardBypassController: KeyguardBypassController,
+        configurationController: ConfigurationController,
+        globalSettings: GlobalSettings,
+        systemClock: SystemClock,
+        executor: DelayableExecutor,
+        accessibilityManagerWrapper: AccessibilityManagerWrapper,
+        uiEventLogger: UiEventLogger,
+        javaAdapter: JavaAdapter,
+        shadeInteractor: ShadeInteractor,
+        avalancheController: AvalancheController
+    ) :
+        HeadsUpManagerPhone(
+            context,
+            headsUpManagerLogger,
+            statusBarStateController,
+            keyguardBypassController,
+            groupManager,
+            visualStabilityProvider,
+            configurationController,
+            mockExecutorHandler(executor),
+            globalSettings,
+            systemClock,
+            executor,
+            accessibilityManagerWrapper,
+            uiEventLogger,
+            javaAdapter,
+            shadeInteractor,
+            avalancheController
+        ) {
+        init {
+            mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME
+            mAutoDismissTime = TEST_AUTO_DISMISS_TIME
+        }
+    }
+
+    private fun createHeadsUpManagerPhone(): HeadsUpManagerPhone {
+        return TestableHeadsUpManagerPhone(
+            mContext,
+            mHeadsUpManagerLogger,
+            mGroupManager,
+            mVSProvider,
+            mStatusBarStateController,
+            mBypassController,
+            mConfigurationController,
+            mGlobalSettings,
+            mSystemClock,
+            mExecutor,
+            mAccessibilityManagerWrapper,
+            mUiEventLogger,
+            mJavaAdapter,
+            mShadeInteractor,
+            mAvalancheController
+        )
+    }
+
+    @Before
+    fun setUp() {
+        whenever(mShadeInteractor.isAnyExpanded).thenReturn(MutableStateFlow(false))
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+        val accessibilityMgr =
+            mDependency.injectMockDependency(AccessibilityManagerWrapper::class.java)
+        whenever(
+                accessibilityMgr.getRecommendedTimeoutMillis(
+                    ArgumentMatchers.anyInt(),
+                    ArgumentMatchers.anyInt()
+                )
+            )
+            .thenReturn(TEST_AUTO_DISMISS_TIME)
+        mDependency.injectMockDependency(NotificationShadeWindowController::class.java)
+        mContext
+            .getOrCreateTestableResources()
+            .addOverride(R.integer.ambient_notification_extension_time, 500)
+        mAvalancheController = AvalancheController(dumpManager, mUiEventLogger, mBgHandler)
+    }
+
+    @Test
+    fun testSnooze() {
+        val hmp: HeadsUpManager = createHeadsUpManagerPhone()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(entry)
+        hmp.snooze()
+        Assert.assertTrue(hmp.isSnoozed(entry.sbn.packageName))
+    }
+
+    @Test
+    fun testSwipedOutNotification() {
+        val hmp: HeadsUpManager = createHeadsUpManagerPhone()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(entry)
+        hmp.addSwipedOutNotification(entry.key)
+
+        // Remove should succeed because the notification is swiped out
+        val removedImmediately = hmp.removeNotification(entry.key, /* releaseImmediately= */ false)
+        Assert.assertTrue(removedImmediately)
+        Assert.assertFalse(hmp.isHeadsUpEntry(entry.key))
+    }
+
+    @Test
+    fun testCanRemoveImmediately_swipedOut() {
+        val hmp: HeadsUpManager = createHeadsUpManagerPhone()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(entry)
+        hmp.addSwipedOutNotification(entry.key)
+
+        // Notification is swiped so it can be immediately removed.
+        Assert.assertTrue(hmp.canRemoveImmediately(entry.key))
+    }
+
+    @Ignore("b/141538055")
+    @Test
+    fun testCanRemoveImmediately_notTopEntry() {
+        val hmp: HeadsUpManager = createHeadsUpManagerPhone()
+        val earlierEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val laterEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext)
+        laterEntry.row = mRow
+        hmp.showNotification(earlierEntry)
+        hmp.showNotification(laterEntry)
+
+        // Notification is "behind" a higher priority notification so we can remove it immediately.
+        Assert.assertTrue(hmp.canRemoveImmediately(earlierEntry.key))
+    }
+
+    @Test
+    fun testExtendHeadsUp() {
+        val hmp = createHeadsUpManagerPhone()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        hmp.showNotification(entry)
+        hmp.extendHeadsUp()
+        mSystemClock.advanceTime((TEST_AUTO_DISMISS_TIME + hmp.mExtensionTime / 2).toLong())
+        Assert.assertTrue(hmp.isHeadsUpEntry(entry.key))
+    }
+
+    companion object {
+        @get:Parameters(name = "{0}")
+        val flags: List<FlagsParameterization>
+            get() = buildList {
+                addAll(FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME))
+                addAll(
+                    FlagsParameterization.allCombinationsOf(NotificationsHeadsUpRefactor.FLAG_NAME)
+                )
+            }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java
index 9e5db73..cd86402 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialog.java
@@ -26,6 +26,7 @@
 @DependsOn(target = Callback.class)
 public interface VolumeDialog extends Plugin {
     String ACTION = "com.android.systemui.action.PLUGIN_VOLUME";
+    String ACTION_VOLUME_UNDO = "com.android.systemui.volume.ACTION_VOLUME_UNDO";
     int VERSION = 1;
 
     void init(int windowType, Callback callback);
diff --git a/packages/SystemUI/res/layout/screenshot.xml b/packages/SystemUI/res/layout/screenshot.xml
deleted file mode 100644
index c134c8e..0000000
--- a/packages/SystemUI/res/layout/screenshot.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2011 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.
-  -->
-<com.android.systemui.screenshot.ScreenshotView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/screenshot_frame"
-    android:theme="@style/FloatingOverlay"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:importantForAccessibility="no">
-    <ImageView
-        android:id="@+id/screenshot_scrolling_scrim"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone"
-        android:clickable="true"
-        android:importantForAccessibility="no"/>
-    <ImageView
-        android:id="@+id/screenshot_flash"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone"
-        android:elevation="7dp"
-        android:src="@android:color/white"/>
-    <include layout="@layout/screenshot_static"
-             android:id="@+id/screenshot_static"/>
-</com.android.systemui.screenshot.ScreenshotView>
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index 12e226a..afd4fa7 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -33,9 +33,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:singleLine="true"
-            android:scrollHorizontally="true"
-            android:ellipsize="marquee"
+            android:layout_marginEnd="@dimen/magnification_setting_view_item_horizontal_spacing"
             android:text="@string/accessibility_magnifier_size"
             android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
             android:focusable="true"
@@ -120,9 +118,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
-            android:singleLine="true"
-            android:scrollHorizontally="true"
-            android:ellipsize="marquee"
+            android:layout_marginEnd="@dimen/magnification_setting_view_item_horizontal_spacing"
             android:text="@string/accessibility_allow_diagonal_scrolling"
             android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
             android:labelFor="@id/magnifier_horizontal_lock_switch"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 80b9ec7..7f7e634 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -104,7 +104,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
+        internet,bt,flashlight,dnd,modes,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
     </string>
 
     <!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 40bdc3e..eda7bb0 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1323,6 +1323,7 @@
     <dimen name="magnifier_drag_handle_padding">3dp</dimen>
     <!-- Magnification settings panel -->
     <dimen name="magnification_setting_view_margin">24dp</dimen>
+    <dimen name="magnification_setting_view_item_horizontal_spacing">12dp</dimen>
     <dimen name="magnification_setting_text_size">18sp</dimen>
     <dimen name="magnification_setting_background_padding">24dp</dimen>
     <dimen name="magnification_setting_background_corner_radius">28dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c1e99db..52e5dea 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -716,6 +716,8 @@
     <!-- QuickSettings: Do not disturb - Priority only [CHAR LIMIT=NONE] -->
     <!-- QuickSettings: Do not disturb - Alarms only [CHAR LIMIT=NONE] -->
     <!-- QuickSettings: Do not disturb - Total silence [CHAR LIMIT=NONE] -->
+    <!-- QuickSettings: Priority modes [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_modes_label">Priority modes</string>
     <!-- QuickSettings: Bluetooth [CHAR LIMIT=NONE] -->
     <string name="quick_settings_bluetooth_label">Bluetooth</string>
     <!-- QuickSettings: Bluetooth (Multiple) [CHAR LIMIT=NONE] -->
@@ -3597,6 +3599,10 @@
          that shows the user which keyboard shortcuts they can use. The "App shortcuts" are
          for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] -->
     <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string>
+    <!-- Default Title of the keyboard shortcut helper category for current app. The helper is a
+         component that shows the user which keyboard shortcuts they can use. The current app
+         shortcuts are shortcuts provided by the currently open app. [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_current_app_shortcuts">Current App</string>
     <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component
          that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts
          are for example "Turn on talkback". [CHAR LIMIT=NONE] -->
@@ -3652,4 +3658,6 @@
     <string name="home_controls_dream_label">Home Controls</string>
     <!-- Description for home control panel [CHAR LIMIT=67] -->
     <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
+    <!-- Label for volume undo action [CHAR LIMIT=NONE] -->
+    <string name="volume_undo_action">Undo</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7475eb2..36912ac 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1604,7 +1604,6 @@
         <item name="android:fontFamily">google-sans</item>
         <item name="android:textColor">?androidprv:attr/textColorPrimary</item>
         <item name="android:textSize">@dimen/magnification_setting_text_size</item>
-        <item name="android:singleLine">true</item>
     </style>
 
     <style name="TextAppearance.MagnificationSetting.EditButton">
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index ad09b46..c702927 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -85,6 +85,16 @@
         <item>On</item>
     </string-array>
 
+    <!-- State names for modes (Priority modes) tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_modes">
+        <item>Unavailable</item>
+        <item>Off</item>
+        <item>On</item>
+    </string-array>
+
     <!-- State names for flashlight tile: unavailable, off, on.
          This subtitle is shown when the tile is in that particular state but does not set its own
          subtitle, so some of these may never appear on screen. They should still be translated as
diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json
new file mode 100644
index 0000000..f10d92a
--- /dev/null
+++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/2.json
@@ -0,0 +1,81 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "02e2da2d36e6955200edd5fb49e63c72",
+    "entities": [
+      {
+        "tableName": "communal_widget_table",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1)",
+        "fields": [
+          {
+            "fieldPath": "uid",
+            "columnName": "uid",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "widgetId",
+            "columnName": "widget_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "componentName",
+            "columnName": "component_name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "itemId",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userSerialNumber",
+            "columnName": "user_serial_number",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "-1"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "uid"
+          ]
+        }
+      },
+      {
+        "tableName": "communal_item_rank_table",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "uid",
+            "columnName": "uid",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "rank",
+            "columnName": "rank",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "uid"
+          ]
+        }
+      }
+    ],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '02e2da2d36e6955200edd5fb49e63c72')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index a42f4c2d..baf8f5a 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -54,7 +54,6 @@
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
-import com.android.systemui.tuner.TunablePadding.TunablePaddingService;
 import com.android.systemui.tuner.TunerService;
 
 import dagger.Lazy;
@@ -129,7 +128,6 @@
     @Nullable
     @Inject Lazy<VolumeDialogController> mVolumeDialogController;
     @Inject Lazy<MetricsLogger> mMetricsLogger;
-    @Inject Lazy<TunablePaddingService> mTunablePaddingService;
     @Inject Lazy<UiOffloadThread> mUiOffloadThread;
     @Inject Lazy<LightBarController> mLightBarController;
     @Inject Lazy<OverviewProxyService> mOverviewProxyService;
@@ -177,7 +175,6 @@
         mProviders.put(FragmentService.class, mFragmentService::get);
         mProviders.put(VolumeDialogController.class, mVolumeDialogController::get);
         mProviders.put(MetricsLogger.class, mMetricsLogger::get);
-        mProviders.put(TunablePaddingService.class, mTunablePaddingService::get);
         mProviders.put(UiOffloadThread.class, mUiOffloadThread::get);
         mProviders.put(LightBarController.class, mLightBarController::get);
         mProviders.put(OverviewProxyService.class, mOverviewProxyService::get);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index b4530ac..ed7062b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -26,6 +26,7 @@
 import android.util.Range;
 import android.view.WindowManager;
 
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.util.settings.SecureSettings;
@@ -40,7 +41,8 @@
 public class MagnificationSettingsController implements ComponentCallbacks {
 
     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
-    private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(1.0f, 8.0f);
+    private static final Range<Float> A11Y_ACTION_SCALE_RANGE =
+        new Range<>(1.0f, MagnificationConstants.SCALE_MAX_VALUE);
 
     private final Context mContext;
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index f2a68a8..5f6f21a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -55,7 +55,6 @@
 import android.widget.LinearLayout;
 import android.widget.SeekBar;
 import android.widget.Switch;
-import android.widget.TextView;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -90,7 +89,6 @@
 
     private SeekBarWithIconButtonsView mZoomSeekbar;
     private LinearLayout mAllowDiagonalScrollingView;
-    private TextView mAllowDiagonalScrollingTitle;
     private Switch mAllowDiagonalScrollingSwitch;
     private LinearLayout mPanelView;
     private LinearLayout mSettingView;
@@ -98,7 +96,6 @@
     private ImageButton mMediumButton;
     private ImageButton mLargeButton;
     private Button mDoneButton;
-    private TextView mSizeTitle;
     private Button mEditButton;
     private ImageButton mFullScreenButton;
     private int mLastSelectedButtonIndex = MagnificationSize.NONE;
@@ -522,11 +519,8 @@
         mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
         mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
         mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
-        mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title);
         mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
         mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);
-        mAllowDiagonalScrollingTitle =
-                mSettingView.findViewById(R.id.magnifier_horizontal_lock_title);
 
         mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider);
         mZoomSeekbar.setMax((int) (mZoomSeekbar.getChangeMagnitude()
@@ -550,8 +544,6 @@
         mDoneButton.setOnClickListener(mButtonClickListener);
         mFullScreenButton.setOnClickListener(mButtonClickListener);
         mEditButton.setOnClickListener(mButtonClickListener);
-        mSizeTitle.setSelected(true);
-        mAllowDiagonalScrollingTitle.setSelected(true);
 
         mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
             // Adds a pending post check to avoiding redundant calculation because this callback
@@ -578,6 +570,7 @@
             // CONFIG_FONT_SCALE: font size change
             // CONFIG_LOCALE: language change
             // CONFIG_DENSITY: display size change
+            mParams.width = getPanelWidth(mContext);
             mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
 
             boolean showSettingPanelAfterConfigChange = mIsVisible;
@@ -660,9 +653,22 @@
         mCallback.onEditMagnifierSizeMode(enable);
     }
 
-    private static LayoutParams createLayoutParams(Context context) {
+    private int getPanelWidth(Context context) {
+        // The magnification settings panel width is limited to the minimum of
+        //     1. display width
+        //     2. panel done button width + left and right padding
+        // So we can directly calculate the proper panel width at runtime
+        int displayWidth = mWindowManager.getCurrentWindowMetrics().getBounds().width();
+        int contentWidth = context.getResources()
+                .getDimensionPixelSize(R.dimen.magnification_setting_button_done_width);
+        int padding = context.getResources()
+                .getDimensionPixelSize(R.dimen.magnification_setting_background_padding);
+        return Math.min(displayWidth, contentWidth + 2 * padding);
+    }
+
+    private LayoutParams createLayoutParams(Context context) {
         final LayoutParams params = new LayoutParams(
-                LayoutParams.WRAP_CONTENT,
+                getPanelWidth(context),
                 LayoutParams.WRAP_CONTENT,
                 LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                 LayoutParams.FLAG_NOT_FOCUSABLE,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 4a28d8b..27ded74 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -515,7 +515,7 @@
         List<ResolveInfo> activities = packageManager.queryIntentActivities(intent,
                 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
         if (!activities.isEmpty()) {
-            mContext.startActivity(intent);
+            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
             mStatusBarManager.collapsePanels();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 961d6aa..f041f4d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -186,7 +186,7 @@
     }
 
     @Override
-    public void onDeviceItemGearClicked(@NonNull  DeviceItem deviceItem, @NonNull View view) {
+    public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
         dismissDialogIfExists();
         Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
         Bundle bundle = new Bundle();
@@ -198,7 +198,7 @@
     }
 
     @Override
-    public void onDeviceItemOnClicked(@NonNull  DeviceItem deviceItem, @NonNull View view) {
+    public void onDeviceItemOnClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
         CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice();
         switch (deviceItem.getType()) {
             case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE ->
@@ -226,8 +226,8 @@
             final int activePresetIndex = mPresetsController.getActivePresetIndex();
             refreshPresetInfoAdapter(presetInfos, activePresetIndex);
             mPresetSpinner.setVisibility(
-                    (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE
-                            : GONE);
+                    (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice()
+                            && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
         });
     }
 
@@ -303,6 +303,11 @@
         mLocalBluetoothManager.getEventManager().unregisterCallback(this);
     }
 
+    @VisibleForTesting
+    void setHearingDevicesPresetsController(HearingDevicesPresetsController controller) {
+        mPresetsController = controller;
+    }
+
     private void setupDeviceListView(SystemUIDialog dialog) {
         mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
         mHearingDeviceItemList = getHearingDevicesList();
@@ -311,12 +316,15 @@
     }
 
     private void setupPresetSpinner(SystemUIDialog dialog) {
-        mPresetsController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback);
+        if (mPresetsController == null) {
+            mPresetsController = new HearingDevicesPresetsController(mProfileManager,
+                    mPresetCallback);
+        }
         final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
                 mHearingDeviceItemList);
         mPresetsController.setActiveHearingDevice(activeHearingDevice);
 
-        mPresetInfoAdapter = new ArrayAdapter<String>(dialog.getContext(),
+        mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
                 R.layout.hearing_devices_preset_spinner_selected,
                 R.id.hearing_devices_preset_option_text);
         mPresetInfoAdapter.setDropDownViewResource(
@@ -350,7 +358,8 @@
         final int activePresetIndex = mPresetsController.getActivePresetIndex();
         refreshPresetInfoAdapter(presetInfos, activePresetIndex);
         mPresetSpinner.setVisibility(
-                (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
+                (activeHearingDevice != null && activeHearingDevice.isConnectedHapClientDevice()
+                        && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
     }
 
     private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt
index cdeeb6f..7abad14 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt
@@ -21,6 +21,9 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
 import com.android.systemui.CoreStartable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -29,6 +32,7 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 
 @SysUISingleton
@@ -38,10 +42,15 @@
     private val broadcastDispatcher: BroadcastDispatcher,
     private val communalInteractor: CommunalInteractor,
     @CommunalLog logBuffer: LogBuffer,
+    private val secureSettings: SecureSettings,
+    handler: Handler,
 ) : CoreStartable, BroadcastReceiver() {
 
     private val logger = Logger(logBuffer, TAG)
 
+    private var oldToNewWidgetIdMap = emptyMap<Int, Int>()
+    private var userSetupComplete = false
+
     override fun start() {
         broadcastDispatcher.registerReceiver(
             receiver = this,
@@ -73,8 +82,53 @@
             return
         }
 
-        val oldToNewWidgetIdMap = oldIds.zip(newIds).toMap()
-        communalInteractor.restoreWidgets(oldToNewWidgetIdMap)
+        oldToNewWidgetIdMap = oldIds.zip(newIds).toMap()
+
+        logger.i({ "On old to new widget ids mapping updated: $str1" }) {
+            str1 = oldToNewWidgetIdMap.toString()
+        }
+
+        maybeRestoreWidgets()
+
+        // Start observing if user setup is not complete
+        if (!userSetupComplete) {
+            startObservingUserSetupComplete()
+        }
+    }
+
+    private val userSetupObserver =
+        object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean) {
+                maybeRestoreWidgets()
+
+                // Stop observing once user setup is complete
+                if (userSetupComplete) {
+                    stopObservingUserSetupComplete()
+                }
+            }
+        }
+
+    private fun maybeRestoreWidgets() {
+        val newValue = secureSettings.getInt(USER_SETUP_COMPLETE) > 0
+
+        if (userSetupComplete != newValue) {
+            userSetupComplete = newValue
+            logger.i({ "User setup complete: $bool1" }) { bool1 = userSetupComplete }
+        }
+
+        if (userSetupComplete && oldToNewWidgetIdMap.isNotEmpty()) {
+            logger.i("Starting to restore widgets")
+            communalInteractor.restoreWidgets(oldToNewWidgetIdMap.toMap())
+            oldToNewWidgetIdMap = emptyMap()
+        }
+    }
+
+    private fun startObservingUserSetupComplete() {
+        secureSettings.registerContentObserverSync(USER_SETUP_COMPLETE, userSetupObserver)
+    }
+
+    private fun stopObservingUserSetupComplete() {
+        secureSettings.unregisterContentObserverSync(userSetupObserver)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
new file mode 100644
index 0000000..78016c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.communal
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.data.repository.CommunalMediaRepository
+import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class CommunalOngoingContentStartable
+@Inject
+constructor(
+    @Background val bgScope: CoroutineScope,
+    private val communalInteractor: CommunalInteractor,
+    private val communalMediaRepository: CommunalMediaRepository,
+    private val communalSmartspaceRepository: CommunalSmartspaceRepository,
+    private val featureFlags: FeatureFlagsClassic,
+) : CoreStartable {
+
+    override fun start() {
+        if (
+            !featureFlags.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) ||
+                !com.android.systemui.Flags.communalHub()
+        ) {
+            return
+        }
+
+        bgScope.launch {
+            communalInteractor.isCommunalEnabled.collect { enabled ->
+                if (enabled) {
+                    communalMediaRepository.startListening()
+                    communalSmartspaceRepository.startListening()
+                } else {
+                    communalMediaRepository.stopListening()
+                    communalSmartspaceRepository.stopListening()
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 88c3f9f6..bde6f42 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -18,7 +18,9 @@
 
 import android.provider.Settings
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.communalHub
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -28,6 +30,8 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dock.DockManager
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -74,6 +78,7 @@
     private val systemSettings: SystemSettings,
     centralSurfacesOpt: Optional<CentralSurfaces>,
     private val notificationShadeWindowController: NotificationShadeWindowController,
+    private val featureFlagsClassic: FeatureFlagsClassic,
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
@@ -86,13 +91,21 @@
 
     private val centralSurfaces: CentralSurfaces? by centralSurfacesOpt
 
+    private val flagEnabled: Boolean by lazy {
+        featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()
+    }
+
     override fun start() {
+        if (!flagEnabled) {
+            return
+        }
+
         // Handle automatically switching based on keyguard state.
         keyguardTransitionInteractor.startedKeyguardTransitionStep
             .mapLatest(::determineSceneAfterTransition)
             .filterNotNull()
-            .onEach { nextScene ->
-                communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade)
+            .onEach { (nextScene, nextTransition) ->
+                communalSceneInteractor.changeScene(nextScene, nextTransition)
             }
             .launchIn(applicationScope)
 
@@ -188,7 +201,7 @@
 
     private suspend fun determineSceneAfterTransition(
         lastStartedTransition: TransitionStep,
-    ): SceneKey? {
+    ): Pair<SceneKey, TransitionKey>? {
         val to = lastStartedTransition.to
         val from = lastStartedTransition.from
         val docked = dockManager.isDocked
@@ -201,22 +214,27 @@
                 // underneath the hub is shown. When launching activities over lockscreen, we only
                 // change scenes once the activity launch animation is finished, so avoid
                 // changing the scene here.
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             }
             to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> {
                 // When transitioning to the hub from an occluded state, fade out the hub without
                 // doing any translation.
-                CommunalScenes.Communal
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.SimpleFade)
             }
             // Transitioning to Blank scene when entering the edit mode will be handled separately
             // with custom animations.
             to == KeyguardState.GONE && !communalInteractor.editModeOpen.value ->
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
                 // If the user taps the screen and wakes the device within this timeout, we don't
                 // want to dismiss the hub
                 delay(AWAKE_DEBOUNCE_DELAY)
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
+            }
+            from == KeyguardState.DOZING && to == KeyguardState.GLANCEABLE_HUB -> {
+                // Make sure the communal hub is showing (immediately, not fading in) when
+                // transitioning from dozing to hub.
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.Immediately)
             }
             else -> null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index 2406cc6..3d201a3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule
+import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -52,6 +53,8 @@
             CommunalWidgetModule::class,
             CommunalPrefsRepositoryModule::class,
             CommunalSettingsRepositoryModule::class,
+            CommunalSmartspaceRepositoryModule::class,
+            CommunalStartableModule::class,
         ]
 )
 interface CommunalModule {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
new file mode 100644
index 0000000..74a2cd3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.communal.dagger
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.CommunalBackupRestoreStartable
+import com.android.systemui.communal.CommunalDreamStartable
+import com.android.systemui.communal.CommunalOngoingContentStartable
+import com.android.systemui.communal.CommunalSceneStartable
+import com.android.systemui.communal.log.CommunalLoggerStartable
+import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface CommunalStartableModule {
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalLoggerStartable::class)
+    fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalSceneStartable::class)
+    fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalDreamStartable::class)
+    fun bindCommunalDreamStartable(impl: CommunalDreamStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalAppWidgetHostStartable::class)
+    fun bindCommunalAppWidgetHostStartable(impl: CommunalAppWidgetHostStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalBackupRestoreStartable::class)
+    fun bindCommunalBackupRestoreStartable(impl: CommunalBackupRestoreStartable): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalOngoingContentStartable::class)
+    fun bindCommunalOngoingContentStartable(impl: CommunalOngoingContentStartable): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt
index a8e5174..c3d2683 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt
@@ -43,11 +43,13 @@
         val widgetsFromDb = runBlocking { database.communalWidgetDao().getWidgets().first() }
         val widgetsState = mutableListOf<CommunalHubState.CommunalWidgetItem>()
         widgetsFromDb.keys.forEach { rankItem ->
+            val widget = widgetsFromDb[rankItem]!!
             widgetsState.add(
                 CommunalHubState.CommunalWidgetItem().apply {
                     rank = rankItem.rank
-                    widgetId = widgetsFromDb[rankItem]!!.widgetId
-                    componentName = widgetsFromDb[rankItem]?.componentName
+                    widgetId = widget.widgetId
+                    componentName = widget.componentName
+                    userSerialNumber = widget.userSerialNumber
                 }
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
index 3ce8109..dff6352 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
@@ -17,17 +17,21 @@
 package com.android.systemui.communal.data.db
 
 import android.content.Context
+import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.room.Database
 import androidx.room.Room
 import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
 import com.android.systemui.res.R
 
-@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 1)
+@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 2)
 abstract class CommunalDatabase : RoomDatabase() {
     abstract fun communalWidgetDao(): CommunalWidgetDao
 
     companion object {
+        private const val TAG = "CommunalDatabase"
         private var instance: CommunalDatabase? = null
 
         /**
@@ -51,7 +55,8 @@
                             context.resources.getString(R.string.config_communalDatabase)
                         )
                         .also { builder ->
-                            builder.fallbackToDestructiveMigration(dropAllTables = false)
+                            builder.addMigrations(MIGRATION_1_2)
+                            builder.fallbackToDestructiveMigration(dropAllTables = true)
                             callback?.let { callback -> builder.addCallback(callback) }
                         }
                         .build()
@@ -64,5 +69,23 @@
         fun setInstance(database: CommunalDatabase) {
             instance = database
         }
+
+        /**
+         * This migration adds a user_serial_number column and sets its default value as
+         * [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED]. Work profile widgets added before the
+         * migration still work as expected, but they would be backed up as personal.
+         */
+        @VisibleForTesting
+        val MIGRATION_1_2 =
+            object : Migration(1, 2) {
+                override fun migrate(db: SupportSQLiteDatabase) {
+                    Log.i(TAG, "Migrating from version 1 to 2")
+                    db.execSQL(
+                        "ALTER TABLE communal_widget_table " +
+                            "ADD COLUMN user_serial_number INTEGER NOT NULL DEFAULT " +
+                            "${CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED}"
+                    )
+                }
+            }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
index 0d5336a..e33aead 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
@@ -29,7 +29,28 @@
     @ColumnInfo(name = "component_name") val componentName: String,
     /** Reference the id of an item persisted in the glanceable hub */
     @ColumnInfo(name = "item_id") val itemId: Long,
-)
+    /**
+     * A serial number of the user that the widget provider is associated with. For example, a work
+     * profile widget.
+     *
+     * A serial number may be different from its user id in that user ids may be recycled but serial
+     * numbers are unique until the device is wiped.
+     *
+     * Most commonly, this value would be 0 for the primary user, and 10 for the work profile.
+     */
+    @ColumnInfo(name = "user_serial_number", defaultValue = "$USER_SERIAL_NUMBER_UNDEFINED")
+    val userSerialNumber: Int,
+) {
+    companion object {
+        /**
+         * The user serial number associated with the widget is undefined.
+         *
+         * This should only happen for widgets migrated from V1 before user serial number was
+         * included in the schema.
+         */
+        const val USER_SERIAL_NUMBER_UNDEFINED = -1
+    }
+}
 
 @Entity(tableName = "communal_item_rank_table")
 data class CommunalItemRank(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index d174fd1..933a25a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.communal.data.db
 
 import android.content.ComponentName
+import android.os.UserManager
 import androidx.room.Dao
 import androidx.room.Delete
 import androidx.room.Query
@@ -26,7 +27,7 @@
 import com.android.systemui.communal.nano.CommunalHubState
 import com.android.systemui.communal.widgets.CommunalWidgetHost
 import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS
-import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
@@ -34,58 +35,96 @@
 import javax.inject.Inject
 import javax.inject.Named
 import javax.inject.Provider
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
 /**
  * Callback that will be invoked when the Room database is created. Then the database will be
  * populated with pre-configured default widgets to be rendered in the glanceable hub.
  */
+@SysUISingleton
 class DefaultWidgetPopulation
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
-    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Background private val bgScope: CoroutineScope,
     private val communalWidgetHost: CommunalWidgetHost,
     private val communalWidgetDaoProvider: Provider<CommunalWidgetDao>,
     @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>,
     @CommunalLog logBuffer: LogBuffer,
+    private val userManager: UserManager,
 ) : RoomDatabase.Callback() {
     companion object {
         private const val TAG = "DefaultWidgetPopulation"
     }
+
     private val logger = Logger(logBuffer, TAG)
 
+    /**
+     * Reason for skipping default widgets population. Do not skip if this value is
+     * [SkipReason.NONE].
+     */
+    private var skipReason = SkipReason.NONE
+
     override fun onCreate(db: SupportSQLiteDatabase) {
         super.onCreate(db)
-        applicationScope.launch {
-            addDefaultWidgets()
-            logger.i("Default widgets were populated in the database.")
-        }
-    }
 
-    // Read default widgets from config.xml and populate the database.
-    private suspend fun addDefaultWidgets() =
-        withContext(bgDispatcher) {
+        if (skipReason != SkipReason.NONE) {
+            logger.i("Skipped populating default widgets. Reason: $skipReason")
+            return
+        }
+
+        bgScope.launch {
+            // Default widgets should be associated with the main user.
+            val user = userManager.mainUser
+
+            if (user == null) {
+                logger.w(
+                    "Skipped populating default widgets. Reason: device does not have a main user"
+                )
+                return@launch
+            }
+
+            val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
+
             defaultWidgets.forEachIndexed { index, name ->
                 val provider = ComponentName.unflattenFromString(name)
                 provider?.let {
-                    val id = communalWidgetHost.allocateIdAndBindWidget(provider)
+                    val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
                     id?.let {
                         communalWidgetDaoProvider
                             .get()
                             .addWidget(
                                 widgetId = id,
-                                provider = provider,
-                                priority = defaultWidgets.size - index
+                                componentName = name,
+                                priority = defaultWidgets.size - index,
+                                userSerialNumber = userSerialNumber,
                             )
                     }
                 }
             }
+
+            logger.i("Populated default widgets in the database.")
         }
+    }
+
+    /**
+     * Skip populating default widgets in the Glanceable Hub when the database is created. This has
+     * no effect if default widgets have been populated already.
+     *
+     * @param skipReason Reason for skipping the default widgets population.
+     */
+    fun skipDefaultWidgetsPopulation(skipReason: SkipReason) {
+        this.skipReason = skipReason
+    }
+
+    /** Reason for skipping default widgets population. */
+    enum class SkipReason {
+        /** Do not skip. */
+        NONE,
+        /** Widgets are restored from a backup. */
+        RESTORED_FROM_BACKUP,
+    }
 }
 
 @Dao
@@ -106,10 +145,16 @@
     fun deleteItemRankById(itemId: Long)
 
     @Query(
-        "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " +
-            "VALUES(:widgetId, :componentName, :itemId)"
+        "INSERT INTO communal_widget_table" +
+            "(widget_id, component_name, item_id, user_serial_number) " +
+            "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber)"
     )
-    fun insertWidget(widgetId: Int, componentName: String, itemId: Long): Long
+    fun insertWidget(
+        widgetId: Int,
+        componentName: String,
+        itemId: Long,
+        userSerialNumber: Int,
+    ): Long
 
     @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
     fun insertItemRank(rank: Int): Long
@@ -132,28 +177,41 @@
     }
 
     @Transaction
-    fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long {
+    fun addWidget(
+        widgetId: Int,
+        provider: ComponentName,
+        priority: Int,
+        userSerialNumber: Int,
+    ): Long {
         return addWidget(
             widgetId = widgetId,
             componentName = provider.flattenToString(),
             priority = priority,
+            userSerialNumber = userSerialNumber,
         )
     }
 
     @Transaction
-    fun addWidget(widgetId: Int, componentName: String, priority: Int): Long {
+    fun addWidget(
+        widgetId: Int,
+        componentName: String,
+        priority: Int,
+        userSerialNumber: Int,
+    ): Long {
         return insertWidget(
             widgetId = widgetId,
             componentName = componentName,
             itemId = insertItemRank(priority),
+            userSerialNumber = userSerialNumber,
         )
     }
 
     @Transaction
     fun deleteWidgetById(widgetId: Int): Boolean {
         val widget =
-            getWidgetByIdNow(widgetId) ?: // no entry to delete from db
-            return false
+            getWidgetByIdNow(widgetId)
+                ?: // no entry to delete from db
+                return false
 
         deleteItemRankById(widget.itemId)
         deleteWidgets(widget)
@@ -166,6 +224,8 @@
         clearCommunalWidgetsTable()
         clearCommunalItemRankTable()
 
-        state.widgets.forEach { addWidget(it.widgetId, it.componentName, it.rank) }
+        state.widgets.forEach {
+            addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt
new file mode 100644
index 0000000..ff9dddd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalSmartspaceTimer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.communal.data.model
+
+import android.widget.RemoteViews
+
+/** Data model of a smartspace timer in the Glanceable Hub. */
+data class CommunalSmartspaceTimer(
+    /** Unique id that identifies the timer. */
+    val smartspaceTargetId: String,
+    /** Timestamp in milliseconds of when the timer was created. */
+    val createdTimestampMillis: Long,
+    /** Remote views for the timer that is rendered in Glanceable Hub. */
+    val remoteViews: RemoteViews,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
index e5a0e50..fe9154c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalMediaRepository.kt
@@ -30,6 +30,12 @@
 /** Encapsulates the state of smartspace in communal. */
 interface CommunalMediaRepository {
     val mediaModel: Flow<CommunalMediaModel>
+
+    /** Start listening for media updates. */
+    fun startListening()
+
+    /** Stop listening for media updates. */
+    fun stopListening()
 }
 
 @SysUISingleton
@@ -38,29 +44,7 @@
 constructor(
     private val mediaDataManager: MediaDataManager,
     @CommunalTableLog tableLogBuffer: TableLogBuffer,
-) : CommunalMediaRepository {
-
-    private val mediaDataListener =
-        object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(
-                key: String,
-                oldKey: String?,
-                data: MediaData,
-                immediately: Boolean,
-                receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
-            ) {
-                updateMediaModel(data)
-            }
-
-            override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
-                updateMediaModel()
-            }
-        }
-
-    init {
-        mediaDataManager.addListener(mediaDataListener)
-    }
+) : CommunalMediaRepository, MediaDataManager.Listener {
 
     private val _mediaModel: MutableStateFlow<CommunalMediaModel> =
         MutableStateFlow(CommunalMediaModel.INACTIVE)
@@ -72,6 +56,29 @@
             initialValue = CommunalMediaModel.INACTIVE,
         )
 
+    override fun startListening() {
+        mediaDataManager.addListener(this)
+    }
+
+    override fun stopListening() {
+        mediaDataManager.removeListener(this)
+    }
+
+    override fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        immediately: Boolean,
+        receivedSmartspaceCardLatency: Int,
+        isSsReactivated: Boolean
+    ) {
+        updateMediaModel(data)
+    }
+
+    override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+        updateMediaModel()
+    }
+
     private fun updateMediaModel(data: MediaData? = null) {
         if (mediaDataManager.hasActiveMediaOrRecommendation()) {
             _mediaModel.value =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt
new file mode 100644
index 0000000..e1d9bef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import android.app.smartspace.SmartspaceTarget
+import android.os.Parcelable
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import com.android.systemui.communal.smartspace.CommunalSmartspaceController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.BcSmartspaceDataPlugin
+import com.android.systemui.util.time.SystemClock
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+interface CommunalSmartspaceRepository {
+    /** Smartspace timer targets for the communal surface. */
+    val timers: Flow<List<CommunalSmartspaceTimer>>
+
+    /** Start listening for smartspace updates. */
+    fun startListening()
+
+    /** Stop listening for smartspace updates. */
+    fun stopListening()
+}
+
+@SysUISingleton
+class CommunalSmartspaceRepositoryImpl
+@Inject
+constructor(
+    private val communalSmartspaceController: CommunalSmartspaceController,
+    @Main private val uiExecutor: Executor,
+    private val systemClock: SystemClock,
+) : CommunalSmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener {
+
+    private val _timers: MutableStateFlow<List<CommunalSmartspaceTimer>> =
+        MutableStateFlow(emptyList())
+    override val timers: Flow<List<CommunalSmartspaceTimer>> = _timers
+
+    private var targetCreationTimes = emptyMap<String, Long>()
+
+    override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) {
+        val targets = targetsNullable?.filterIsInstance<SmartspaceTarget>() ?: emptyList()
+        val timerTargets =
+            targets
+                .filter { target ->
+                    target.featureType == SmartspaceTarget.FEATURE_TIMER &&
+                        target.remoteViews != null
+                }
+                .associateBy { stableId(it.smartspaceTargetId) }
+
+        // The creation times from smartspace targets are unreliable (b/318535930). Therefore,
+        // SystemUI uses the timestamp of which a timer first appears, and caches these values to
+        // prevent timers from swapping positions in the hub.
+        targetCreationTimes =
+            timerTargets.mapValues { (stableId, _) ->
+                targetCreationTimes[stableId] ?: systemClock.currentTimeMillis()
+            }
+
+        _timers.value =
+            timerTargets.map { (stableId, target) ->
+                CommunalSmartspaceTimer(
+                    // The view layer should have the instance based smartspaceTargetId instead of
+                    // stable id, so that when a new instance of the timer is created, for example,
+                    // when it is paused, the view should re-render its remote views.
+                    smartspaceTargetId = target.smartspaceTargetId,
+                    createdTimestampMillis = targetCreationTimes[stableId]!!,
+                    remoteViews = target.remoteViews!!,
+                )
+            }
+    }
+
+    override fun startListening() {
+        if (android.app.smartspace.flags.Flags.remoteViews()) {
+            uiExecutor.execute {
+                communalSmartspaceController.addListener(
+                    listener = this@CommunalSmartspaceRepositoryImpl
+                )
+            }
+        }
+    }
+
+    override fun stopListening() {
+        uiExecutor.execute {
+            communalSmartspaceController.removeListener(
+                listener = this@CommunalSmartspaceRepositoryImpl
+            )
+        }
+    }
+
+    companion object {
+        /**
+         * The smartspace target id is instance-based, meaning a single timer (from the user's
+         * perspective) can have multiple instances. For example, when a timer is paused, a new
+         * instance is created. To address this, SystemUI manually removes the instance id to
+         * maintain a consistent id across sessions.
+         *
+         * It is assumed that timer target ids follow this format: timer-${stableId}-${instanceId}.
+         * This function returns timer-${stableId}, stripping out the instance id.
+         */
+        @VisibleForTesting
+        fun stableId(targetId: String): String {
+            return targetId.split("-").take(2).joinToString("-")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryModule.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt
rename to packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryModule.kt
index c77bcc5..b11c6d6 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryModule.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package com.android.systemui.communal.data.repository
 
 import dagger.Binds
 import dagger.Module
 
 @Module
-interface SmartspaceRepositoryModule {
-    @Binds fun smartspaceRepository(impl: SmartspaceRepositoryImpl): SmartspaceRepository
+interface CommunalSmartspaceRepositoryModule {
+    @Binds
+    fun communalSmartspaceRepository(
+        impl: CommunalSmartspaceRepositoryImpl
+    ): CommunalSmartspaceRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index fdb797d..e65e5e5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -20,10 +20,14 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
 import android.os.UserHandle
+import android.os.UserManager
 import com.android.systemui.common.data.repository.PackageChangeRepository
 import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.communal.data.backup.CommunalBackupUtils
 import com.android.systemui.communal.data.db.CommunalWidgetDao
+import com.android.systemui.communal.data.db.CommunalWidgetItem
+import com.android.systemui.communal.data.db.DefaultWidgetPopulation
+import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason.RESTORED_FROM_BACKUP
 import com.android.systemui.communal.nano.CommunalHubState
 import com.android.systemui.communal.proto.toCommunalHubState
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -98,6 +102,8 @@
     private val backupManager: BackupManager,
     private val backupUtils: CommunalBackupUtils,
     packageChangeRepository: PackageChangeRepository,
+    private val userManager: UserManager,
+    private val defaultWidgetPopulation: DefaultWidgetPopulation,
 ) : CommunalWidgetRepository {
     companion object {
         const val TAG = "CommunalWidgetRepository"
@@ -185,6 +191,7 @@
                     widgetId = id,
                     provider = provider,
                     priority = priority,
+                    userSerialNumber = userManager.getUserSerialNumber(user.identifier),
                 )
                 backupManager.dataChanged()
             } else {
@@ -228,9 +235,38 @@
                 return@launch
             }
 
+            // Abort restoring widgets if this code is somehow run on a device that does not have
+            // a main user, e.g. auto.
+            val mainUser = userManager.mainUser
+            if (mainUser == null) {
+                logger.w("Skipped restoring widgets because device does not have a main user")
+                return@launch
+            }
+
             val widgetsWithHost = appWidgetHost.appWidgetIds.toList()
             val widgetsToRemove = widgetsWithHost.toMutableList()
 
+            val oldUserSerialNumbers = state.widgets.map { it.userSerialNumber }.distinct()
+            val usersMap =
+                oldUserSerialNumbers.associateWith { oldUserSerialNumber ->
+                    if (oldUserSerialNumber == CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED) {
+                        // If user serial number from the backup is undefined, the widget was added
+                        // to the hub before user serial numbers are stored in the database. In this
+                        // case, we restore the widget with the main user.
+                        mainUser
+                    } else {
+                        // If the user serial number is defined, look up whether the user is
+                        // restored. This API returns a user handle matching its backed up user
+                        // serial number, if the user is restored. Otherwise, null is returned.
+                        backupManager.getUserForAncestralSerialNumber(oldUserSerialNumber.toLong())
+                            ?: null
+                    }
+                }
+            logger.d({ "Restored users map: $str1" }) { str1 = usersMap.toString() }
+
+            // A set to hold all widgets that belong to non-main users
+            val secondaryUserWidgets = mutableSetOf<CommunalHubState.CommunalWidgetItem>()
+
             // Produce a new state to be restored, skipping invalid widgets
             val newWidgets =
                 state.widgets.mapNotNull { restoredWidget ->
@@ -249,20 +285,67 @@
                         return@mapNotNull null
                     }
 
+                    // Skip if user / profile is not registered
+                    val newUser = usersMap[restoredWidget.userSerialNumber]
+                    if (newUser == null) {
+                        logger.d({
+                            "Skipped restoring widget $int1 because its user $int2 is not " +
+                                "registered"
+                        }) {
+                            int1 = restoredWidget.widgetId
+                            int2 = restoredWidget.userSerialNumber
+                        }
+                        return@mapNotNull null
+                    }
+
+                    // Place secondary user widgets in a bucket to be manually bound later because
+                    // of a platform bug (b/349852237) that backs up work profile widgets as
+                    // personal.
+                    if (newUser.identifier != mainUser.identifier) {
+                        logger.d({
+                            "Skipped restoring widget $int1 for now because its new user $int2 " +
+                                "is secondary. This widget will be bound later."
+                        }) {
+                            int1 = restoredWidget.widgetId
+                            int2 = newUser.identifier
+                        }
+                        secondaryUserWidgets.add(restoredWidget)
+                        return@mapNotNull null
+                    }
+
                     widgetsToRemove.remove(newWidgetId)
 
                     CommunalHubState.CommunalWidgetItem().apply {
                         widgetId = newWidgetId
                         componentName = restoredWidget.componentName
                         rank = restoredWidget.rank
+                        userSerialNumber = userManager.getUserSerialNumber(newUser.identifier)
                     }
                 }
             val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() }
 
+            // Skip default widgets population
+            defaultWidgetPopulation.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP)
+
             // Restore database
-            logger.i("Restoring communal database $newState")
+            logger.i("Restoring communal database:\n$newState")
             communalWidgetDao.restoreCommunalHubState(newState)
 
+            // Manually bind each secondary user widget due to platform bug b/349852237
+            secondaryUserWidgets.forEach { widget ->
+                val newUser = usersMap[widget.userSerialNumber]!!
+                logger.i({ "Binding secondary user ($int1) widget $int2: $str1" }) {
+                    int1 = newUser.identifier
+                    int2 = widget.widgetId
+                    str1 = widget.componentName
+                }
+                addWidget(
+                    provider = ComponentName.unflattenFromString(widget.componentName)!!,
+                    user = newUser,
+                    priority = widget.rank,
+                )
+            }
+
             // Delete restored state file from disk
             backupUtils.clear()
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 86f5fe1..3fffd76 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.communal.domain.interactor
 
-import android.app.smartspace.SmartspaceTarget
 import android.content.ComponentName
 import android.content.Intent
 import android.content.IntentFilter
@@ -29,6 +28,7 @@
 import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.data.repository.CommunalMediaRepository
+import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent
@@ -60,7 +60,6 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.smartspace.data.repository.SmartspaceRepository
 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.emitOnStart
@@ -82,7 +81,6 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -101,7 +99,7 @@
     private val widgetRepository: CommunalWidgetRepository,
     private val communalPrefsInteractor: CommunalPrefsInteractor,
     private val mediaRepository: CommunalMediaRepository,
-    smartspaceRepository: SmartspaceRepository,
+    private val smartspaceRepository: CommunalSmartspaceRepository,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     communalSettingsInteractor: CommunalSettingsInteractor,
@@ -435,19 +433,6 @@
             }
         }
 
-    /** A flow of available smartspace targets. Currently only showing timers. */
-    private val smartspaceTargets: Flow<List<SmartspaceTarget>> =
-        if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) {
-            flowOf(emptyList())
-        } else {
-            smartspaceRepository.communalSmartspaceTargets.map { targets ->
-                targets.filter { target ->
-                    target.featureType == SmartspaceTarget.FEATURE_TIMER &&
-                        target.remoteViews != null
-                }
-            }
-        }
-
     /** CTA tile to be displayed in the glanceable hub (view mode). */
     val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> =
         communalPrefsInteractor.isCtaDismissed.map { isDismissed ->
@@ -472,16 +457,16 @@
      * sized dynamically.
      */
     fun getOngoingContent(mediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> =
-        combine(smartspaceTargets, mediaRepository.mediaModel) { smartspace, media ->
+        combine(smartspaceRepository.timers, mediaRepository.mediaModel) { timers, media ->
                 val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>()
 
-                // Add smartspace
+                // Add smartspace timers
                 ongoingContent.addAll(
-                    smartspace.map { target ->
+                    timers.map { timer ->
                         CommunalContentModel.Smartspace(
-                            smartspaceTargetId = target.smartspaceTargetId,
-                            remoteViews = target.remoteViews!!,
-                            createdTimestampMillis = target.creationTimeMillis,
+                            smartspaceTargetId = timer.smartspaceTargetId,
+                            remoteViews = timer.remoteViews,
+                            createdTimestampMillis = timer.createdTimestampMillis,
                         )
                     }
                 )
@@ -556,4 +541,29 @@
             )
         }
     }
+
+    /**
+     * {@link #setScrollPosition} persists the current communal grid scroll position (to volatile
+     * memory) so that the next presentation of the grid (either as glanceable hub or edit mode) can
+     * restore position.
+     */
+    fun setScrollPosition(firstVisibleItemIndex: Int, firstVisibleItemOffset: Int) {
+        _firstVisibleItemIndex = firstVisibleItemIndex
+        _firstVisibleItemOffset = firstVisibleItemOffset
+    }
+
+    fun resetScrollPosition() {
+        _firstVisibleItemIndex = 0
+        _firstVisibleItemOffset = 0
+    }
+
+    val firstVisibleItemIndex: Int
+        get() = _firstVisibleItemIndex
+
+    private var _firstVisibleItemIndex: Int = 0
+
+    val firstVisibleItemOffset: Int
+        get() = _firstVisibleItemOffset
+
+    private var _firstVisibleItemOffset: Int = 0
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
index 0816259..bc14ae1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
+++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
@@ -35,5 +35,8 @@
 
         // Rank or order of the widget in the communal hub.
         int32 rank = 3;
+
+        // Serial number of the user associated with the widget.
+        int32 user_serial_number = 4;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 6ec6ec1..19d7ceb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -59,6 +59,18 @@
     /** Accessibility delegate to be set on CommunalAppWidgetHostView. */
     open val widgetAccessibilityDelegate: View.AccessibilityDelegate? = null
 
+    /**
+     * The up-to-date value of the grid scroll offset. persisted to interactor on
+     * {@link #persistScrollPosition}
+     */
+    private var currentScrollOffset = 0
+
+    /**
+     * The up-to-date value of the grid scroll index. persisted to interactor on
+     * {@link #persistScrollPosition}
+     */
+    private var currentScrollIndex = 0
+
     fun signalUserInteraction() {
         communalInteractor.signalUserInteraction()
     }
@@ -147,6 +159,28 @@
     /** Called as the user request to show the customize widget button. */
     open fun onLongClick() {}
 
+    /** Called when the grid scroll position has been updated. */
+    open fun onScrollPositionUpdated(firstVisibleItemIndex: Int, firstVisibleItemScroll: Int) {
+        currentScrollIndex = firstVisibleItemIndex
+        currentScrollOffset = firstVisibleItemScroll
+    }
+
+    /** Stores scroll values to interactor. */
+    protected fun persistScrollPosition() {
+        communalInteractor.setScrollPosition(currentScrollIndex, currentScrollOffset)
+    }
+
+    /** Invoked after scroll values are used to initialize grid position. */
+    open fun clearPersistedScrollPosition() {
+        communalInteractor.setScrollPosition(0, 0)
+    }
+
+    val savedFirstScrollIndex: Int
+        get() = communalInteractor.firstVisibleItemIndex
+
+    val savedFirstScrollOffset: Int
+        get() = communalInteractor.firstVisibleItemOffset
+
     /** Set the key of the currently selected item */
     fun setSelectedKey(key: String?) {
         _selectedKey.value = key
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 91f4c1c..7b0aadf 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -220,6 +220,9 @@
     /** Called when exiting the edit mode, before transitioning back to the communal scene. */
     fun cleanupEditModeState() {
         communalSceneInteractor.setEditModeState(null)
+
+        // Set the scroll position of the glanceable hub to match where we are now.
+        persistScrollPosition()
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 780bf70..02ecfe1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -233,7 +233,10 @@
 
     override fun onOpenWidgetEditor(
         shouldOpenWidgetPickerOnStart: Boolean,
-    ) = communalInteractor.showWidgetEditor(selectedKey.value, shouldOpenWidgetPickerOnStart)
+    ) {
+        persistScrollPosition()
+        communalInteractor.showWidgetEditor(selectedKey.value, shouldOpenWidgetPickerOnStart)
+    }
 
     override fun onDismissCtaTile() {
         scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
index 2000f96..684303ae 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
@@ -43,12 +43,6 @@
 
         @SysUISingleton
         @Provides
-        fun provideAppWidgetManager(@Application context: Context): Optional<AppWidgetManager> {
-            return Optional.ofNullable(AppWidgetManager.getInstance(context))
-        }
-
-        @SysUISingleton
-        @Provides
         fun provideCommunalAppWidgetHost(
             @Application context: Context,
             @Background backgroundScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index b0fc60e..2ea27b7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.dagger;
 
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
+
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
@@ -39,6 +42,7 @@
 import android.app.role.RoleManager;
 import android.app.smartspace.SmartspaceManager;
 import android.app.trust.TrustManager;
+import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.companion.virtual.VirtualDeviceManager;
@@ -111,6 +115,9 @@
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
 import androidx.core.app.NotificationManagerCompat;
 
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
+import com.android.app.viewcapture.ViewCaptureFactory;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.appwidget.IAppWidgetService;
 import com.android.internal.jank.InteractionJankMonitor;
@@ -125,6 +132,7 @@
 import com.android.systemui.user.utils.UserScopedService;
 import com.android.systemui.user.utils.UserScopedServiceImpl;
 
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -367,6 +375,12 @@
 
     @Provides
     @Singleton
+    static Optional<AppWidgetManager> provideAppWidgetManager(@Application Context context) {
+        return Optional.ofNullable(AppWidgetManager.getInstance(context));
+    }
+
+    @Provides
+    @Singleton
     static IAppWidgetService provideIAppWidgetService() {
         return IAppWidgetService.Stub.asInterface(
                 ServiceManager.getService(Context.APPWIDGET_SERVICE));
@@ -680,6 +694,15 @@
 
     @Provides
     @Singleton
+    static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager(
+            WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) {
+        return new ViewCaptureAwareWindowManager(windowManager,
+                /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture),
+                /* isViewCaptureEnabled= */ enableViewCaptureTracing());
+    }
+
+    @Provides
+    @Singleton
     static PermissionManager providePermissionManager(Context context) {
         PermissionManager pm = context.getSystemService(PermissionManager.class);
         if (pm != null) {
@@ -764,4 +787,10 @@
         return IDeviceIdleController.Stub.asInterface(
                 ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
     }
+
+    @Provides
+    @Singleton
+    static ViewCapture provideViewCapture(Context context) {
+        return ViewCaptureFactory.getInstance(context);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 593196c..88601da 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -24,11 +24,6 @@
 import com.android.systemui.back.domain.interactor.BackActionInteractor
 import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
-import com.android.systemui.communal.CommunalDreamStartable
-import com.android.systemui.communal.CommunalBackupRestoreStartable
-import com.android.systemui.communal.CommunalSceneStartable
-import com.android.systemui.communal.log.CommunalLoggerStartable
-import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
@@ -80,12 +75,13 @@
  * @deprecated
  */
 @Module(
-    includes = [
-        MultiUserUtilsModule::class,
-        StartControlsStartableModule::class,
-        StartBinderLoggerModule::class,
-        WallpaperModule::class,
-    ]
+    includes =
+        [
+            MultiUserUtilsModule::class,
+            StartControlsStartableModule::class,
+            StartBinderLoggerModule::class,
+            WallpaperModule::class,
+        ]
 )
 abstract class SystemUICoreStartableModule {
     /** Inject into BiometricNotificationService */
@@ -96,25 +92,25 @@
         service: BiometricNotificationService
     ): CoreStartable
 
-    /** Inject into ClipboardListener.  */
+    /** Inject into ClipboardListener. */
     @Binds
     @IntoMap
     @ClassKey(ClipboardListener::class)
     abstract fun bindClipboardListener(sysui: ClipboardListener): CoreStartable
 
-    /** Inject into GlobalActionsComponent.  */
+    /** Inject into GlobalActionsComponent. */
     @Binds
     @IntoMap
     @ClassKey(GlobalActionsComponent::class)
     abstract fun bindGlobalActionsComponent(sysui: GlobalActionsComponent): CoreStartable
 
-    /** Inject into InstantAppNotifier.  */
+    /** Inject into InstantAppNotifier. */
     @Binds
     @IntoMap
     @ClassKey(InstantAppNotifier::class)
     abstract fun bindInstantAppNotifier(sysui: InstantAppNotifier): CoreStartable
 
-    /** Inject into KeyboardUI.  */
+    /** Inject into KeyboardUI. */
     @Binds
     @IntoMap
     @ClassKey(KeyboardUI::class)
@@ -125,7 +121,7 @@
     @IntoMap
     @ClassKey(MediaProjectionTaskSwitcherCoreStartable::class)
     abstract fun bindProjectedTaskListener(
-            sysui: MediaProjectionTaskSwitcherCoreStartable
+        sysui: MediaProjectionTaskSwitcherCoreStartable
     ): CoreStartable
 
     /** Inject into KeyguardBiometricLockoutLogger */
@@ -136,38 +132,38 @@
         sysui: KeyguardBiometricLockoutLogger
     ): CoreStartable
 
-    /** Inject into KeyguardViewMediator.  */
+    /** Inject into KeyguardViewMediator. */
     @Binds
     @IntoMap
     @ClassKey(KeyguardViewMediator::class)
     abstract fun bindKeyguardViewMediator(sysui: KeyguardViewMediator): CoreStartable
 
-    /** Inject into LatencyTests.  */
+    /** Inject into LatencyTests. */
     @Binds
     @IntoMap
     @ClassKey(LatencyTester::class)
     abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable
 
-    /** Inject into DisplaySwitchLatencyTracker.  */
+    /** Inject into DisplaySwitchLatencyTracker. */
     @Binds
     @IntoMap
     @ClassKey(DisplaySwitchLatencyTracker::class)
     abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable
 
-    /** Inject into NotificationChannels.  */
+    /** Inject into NotificationChannels. */
     @Binds
     @IntoMap
     @ClassKey(NotificationChannels::class)
     @PerUser
     abstract fun bindNotificationChannels(sysui: NotificationChannels): CoreStartable
 
-    /** Inject into ImmersiveModeConfirmation.  */
+    /** Inject into ImmersiveModeConfirmation. */
     @Binds
     @IntoMap
     @ClassKey(ImmersiveModeConfirmation::class)
     abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable
 
-    /** Inject into RingtonePlayer.  */
+    /** Inject into RingtonePlayer. */
     @Binds
     @IntoMap
     @ClassKey(RingtonePlayer::class)
@@ -179,50 +175,49 @@
     @ClassKey(GesturePointerEventListener::class)
     abstract fun bindGesturePointerEventListener(sysui: GesturePointerEventListener): CoreStartable
 
-    /** Inject into SessionTracker.  */
+    /** Inject into SessionTracker. */
     @Binds
     @IntoMap
     @ClassKey(SessionTracker::class)
     abstract fun bindSessionTracker(service: SessionTracker): CoreStartable
 
-    /** Inject into ShortcutKeyDispatcher.  */
+    /** Inject into ShortcutKeyDispatcher. */
     @Binds
     @IntoMap
     @ClassKey(ShortcutKeyDispatcher::class)
     abstract fun bindShortcutKeyDispatcher(sysui: ShortcutKeyDispatcher): CoreStartable
 
-    /** Inject into SliceBroadcastRelayHandler.  */
+    /** Inject into SliceBroadcastRelayHandler. */
     @Binds
     @IntoMap
     @ClassKey(SliceBroadcastRelayHandler::class)
     abstract fun bindSliceBroadcastRelayHandler(sysui: SliceBroadcastRelayHandler): CoreStartable
 
-    /** Inject into StorageNotification.  */
+    /** Inject into StorageNotification. */
     @Binds
     @IntoMap
     @ClassKey(StorageNotification::class)
     abstract fun bindStorageNotification(sysui: StorageNotification): CoreStartable
 
-    /** Inject into ThemeOverlayController.  */
+    /** Inject into ThemeOverlayController. */
     @Binds
     @IntoMap
     @ClassKey(ThemeOverlayController::class)
     abstract fun bindThemeOverlayController(sysui: ThemeOverlayController): CoreStartable
 
-
-    /** Inject into MediaOutputSwitcherDialogUI.  */
+    /** Inject into MediaOutputSwitcherDialogUI. */
     @Binds
     @IntoMap
     @ClassKey(MediaOutputSwitcherDialogUI::class)
     abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable
 
-    /** Inject into Magnification.  */
+    /** Inject into Magnification. */
     @Binds
     @IntoMap
     @ClassKey(Magnification::class)
     abstract fun bindMagnification(sysui: Magnification): CoreStartable
 
-    /** Inject into WMShell.  */
+    /** Inject into WMShell. */
     @Binds
     @IntoMap
     @ClassKey(WMShell::class)
@@ -239,7 +234,7 @@
     @IntoMap
     @ClassKey(MediaTttChipControllerReceiver::class)
     abstract fun bindMediaTttChipControllerReceiver(
-            sysui: MediaTttChipControllerReceiver
+        sysui: MediaTttChipControllerReceiver
     ): CoreStartable
 
     /** Inject into MediaTttCommandLineHelper. */
@@ -254,8 +249,6 @@
     @ClassKey(ChipbarCoordinator::class)
     abstract fun bindChipbarController(sysui: ChipbarCoordinator): CoreStartable
 
-
-
     /** Inject into StylusUsiPowerStartable) */
     @Binds
     @IntoMap
@@ -267,21 +260,21 @@
     @ClassKey(PhysicalKeyboardCoreStartable::class)
     abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable
 
-    /** Inject into MuteQuickAffordanceCoreStartable*/
+    /** Inject into MuteQuickAffordanceCoreStartable */
     @Binds
     @IntoMap
     @ClassKey(MuteQuickAffordanceCoreStartable::class)
     abstract fun bindMuteQuickAffordanceCoreStartable(
-            sysui: MuteQuickAffordanceCoreStartable
+        sysui: MuteQuickAffordanceCoreStartable
     ): CoreStartable
 
-    /**Inject into DreamMonitor */
+    /** Inject into DreamMonitor */
     @Binds
     @IntoMap
     @ClassKey(DreamMonitor::class)
     abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable
 
-    /**Inject into AssistantAttentionMonitor */
+    /** Inject into AssistantAttentionMonitor */
     @Binds
     @IntoMap
     @ClassKey(AssistantAttentionMonitor::class)
@@ -321,35 +314,6 @@
 
     @Binds
     @IntoMap
-    @ClassKey(CommunalLoggerStartable::class)
-    abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
-
-    @Binds
-    @IntoMap
-    @ClassKey(CommunalSceneStartable::class)
-    abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable
-
-    @Binds
-    @IntoMap
-    @ClassKey(CommunalDreamStartable::class)
-    abstract fun bindCommunalDreamStartable(impl: CommunalDreamStartable): CoreStartable
-
-    @Binds
-    @IntoMap
-    @ClassKey(CommunalAppWidgetHostStartable::class)
-    abstract fun bindCommunalAppWidgetHostStartable(
-        impl: CommunalAppWidgetHostStartable
-    ): CoreStartable
-
-    @Binds
-    @IntoMap
-    @ClassKey(CommunalBackupRestoreStartable::class)
-    abstract fun bindCommunalBackupRestoreStartable(
-        impl: CommunalBackupRestoreStartable
-    ): CoreStartable
-
-    @Binds
-    @IntoMap
     @ClassKey(HomeControlsDreamStartable::class)
     abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index c6c57479..83fa001 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -45,6 +45,7 @@
 import androidx.lifecycle.ServiceLifecycleDispatcher;
 import androidx.lifecycle.ViewModelStore;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
@@ -97,7 +98,7 @@
     @Nullable
     private final ComponentName mHomeControlPanelDreamComponent;
     private final UiEventLogger mUiEventLogger;
-    private final WindowManager mWindowManager;
+    private final ViewCaptureAwareWindowManager mWindowManager;
     private final String mWindowTitle;
 
     // A reference to the {@link Window} used to hold the dream overlay.
@@ -244,7 +245,7 @@
             Context context,
             DreamOverlayLifecycleOwner lifecycleOwner,
             @Main DelayableExecutor executor,
-            WindowManager windowManager,
+            ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
             ComplicationComponent.Factory complicationComponentFactory,
             com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
                     dreamComplicationComponentFactory,
@@ -267,7 +268,7 @@
         super(executor);
         mContext = context;
         mExecutor = executor;
-        mWindowManager = windowManager;
+        mWindowManager = viewCaptureAwareWindowManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mScrimManager = scrimManager;
         mLowLightDreamComponent = lowLightDreamComponent;
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
index 1f0aef8..906f600 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.Flags.keyboardShortcutHelperRewrite
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
 import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.InputShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
 import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
@@ -55,6 +57,10 @@
     fun multitaskingShortcutsSource(impl: MultitaskingShortcutsSource): KeyboardShortcutGroupsSource
 
     @Binds
+    @CurrentAppShortcuts
+    fun currentAppShortcutsSource(impl: CurrentAppShortcutsSource): KeyboardShortcutGroupsSource
+
+    @Binds
     @InputShortcuts
     fun inputShortcutsSources(impl: InputShortcutsSource): KeyboardShortcutGroupsSource
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 7b0c25e..49817b2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -20,6 +20,7 @@
 import android.graphics.drawable.Icon
 import android.hardware.input.InputManager
 import android.util.Log
+import android.view.InputDevice
 import android.view.KeyCharacterMap
 import android.view.KeyEvent
 import android.view.KeyboardShortcutGroup
@@ -28,16 +29,18 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
 import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts
+import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
 import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
 import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.APP_CATEGORIES
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.IME
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MULTI_TASKING
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.SYSTEM
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.CurrentApp
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
@@ -45,7 +48,11 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
@@ -53,11 +60,13 @@
 @Inject
 constructor(
     private val context: Context,
+    @Background private val backgroundScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource,
     @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource,
     @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource,
     @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource,
+    @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource,
     private val inputManager: InputManager,
     stateRepository: ShortcutHelperStateRepository
 ) {
@@ -71,61 +80,82 @@
             }
         }
 
-    val systemShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    SYSTEM,
-                    systemShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
+    val categories: Flow<List<ShortcutCategory>> =
+        activeInputDevice
+            .map {
+                if (it == null) {
+                    return@map emptyList()
+                }
+                return@map listOfNotNull(
+                    fetchSystemShortcuts(it),
+                    fetchMultiTaskingShortcuts(it),
+                    fetchAppCategoriesShortcuts(it),
+                    fetchImeShortcuts(it),
+                    fetchCurrentAppShortcuts(it),
                 )
-            } else {
-                null
             }
-        }
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Lazily,
+                initialValue = emptyList(),
+            )
 
-    val multitaskingShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    MULTI_TASKING,
-                    multitaskingShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
-                )
-            } else {
-                null
-            }
-        }
+    private suspend fun fetchSystemShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            System,
+            systemShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
 
-    val appCategoriesShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    APP_CATEGORIES,
-                    appCategoriesShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = true,
-                )
-            } else {
-                null
-            }
-        }
+    private suspend fun fetchMultiTaskingShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            MultiTasking,
+            multitaskingShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
 
-    val imeShortcutsCategory =
-        activeInputDevice.map {
-            if (it != null) {
-                toShortcutCategory(
-                    it.keyCharacterMap,
-                    IME,
-                    inputShortcutsSource.shortcutGroups(it.id),
-                    keepIcons = false,
-                )
-            } else {
-                null
-            }
+    private suspend fun fetchAppCategoriesShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            AppCategories,
+            appCategoriesShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = true,
+        )
+
+    private suspend fun fetchImeShortcuts(inputDevice: InputDevice) =
+        toShortcutCategory(
+            inputDevice.keyCharacterMap,
+            InputMethodEditor,
+            inputShortcutsSource.shortcutGroups(inputDevice.id),
+            keepIcons = false,
+        )
+
+    private suspend fun fetchCurrentAppShortcuts(inputDevice: InputDevice): ShortcutCategory? {
+        val shortcutGroups = currentAppShortcutsSource.shortcutGroups(inputDevice.id)
+        val categoryType = getCurrentAppShortcutCategoryType(shortcutGroups)
+        return if (categoryType == null) {
+            null
+        } else {
+            toShortcutCategory(
+                inputDevice.keyCharacterMap,
+                categoryType,
+                shortcutGroups,
+                keepIcons = false
+            )
         }
+    }
+
+    private fun getCurrentAppShortcutCategoryType(
+        shortcutGroups: List<KeyboardShortcutGroup>
+    ): ShortcutCategoryType? {
+        return if (shortcutGroups.isEmpty()) {
+            null
+        } else {
+            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
+        }
+    }
 
     private fun toShortcutCategory(
         keyCharacterMap: KeyCharacterMap,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt
new file mode 100644
index 0000000..7e6ed19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSource.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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.keyboard.shortcut.data.source
+
+import android.view.KeyboardShortcutGroup
+import android.view.WindowManager
+import android.view.WindowManager.KeyboardShortcutsReceiver
+import javax.inject.Inject
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+class CurrentAppShortcutsSource @Inject constructor(private val windowManager: WindowManager) :
+    KeyboardShortcutGroupsSource {
+    override suspend fun shortcutGroups(deviceId: Int): List<KeyboardShortcutGroup> =
+        suspendCancellableCoroutine { continuation ->
+            val shortcutsReceiver = KeyboardShortcutsReceiver {
+                continuation.resumeWith(Result.success(it ?: emptyList()))
+            }
+            windowManager.requestAppKeyboardShortcuts(shortcutsReceiver, deviceId)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt
index aba4415..1b20986 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSource.kt
@@ -59,7 +59,7 @@
     private suspend fun getImeShortcutGroup(deviceId: Int): List<KeyboardShortcutGroup> =
         suspendCancellableCoroutine { continuation ->
             val shortcutsReceiver = KeyboardShortcutsReceiver {
-                continuation.resumeWith(Result.success(it))
+                continuation.resumeWith(Result.success(it ?: emptyList()))
             }
             windowManager.requestImeKeyboardShortcuts(shortcutsReceiver, deviceId)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
index d41d21a..6f19561 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 
 @SysUISingleton
 class ShortcutHelperCategoriesInteractor
@@ -33,13 +33,8 @@
 ) {
 
     val shortcutCategories: Flow<List<ShortcutCategory>> =
-        combine(
-            categoriesRepository.systemShortcutsCategory,
-            categoriesRepository.multitaskingShortcutsCategory,
-            categoriesRepository.imeShortcutsCategory,
-            categoriesRepository.appCategoriesShortcutsCategory,
-        ) { shortcutCategories ->
-            shortcutCategories.filterNotNull().map { groupSubCategoriesInCategory(it) }
+        categoriesRepository.categories.map { categories ->
+            categories.map { category -> groupSubCategoriesInCategory(category) }
         }
 
     private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt
index 0e4c923..51631b1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CurrentAppShortcuts.kt
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package com.android.systemui.keyboard.shortcut.qualifiers
 
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import javax.inject.Qualifier
 
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+@Qualifier annotation class CurrentAppShortcuts
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index 63e167a..4eabefc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -16,11 +16,16 @@
 
 package com.android.systemui.keyboard.shortcut.shared.model
 
-enum class ShortcutCategoryType {
-    SYSTEM,
-    MULTI_TASKING,
-    IME,
-    APP_CATEGORIES,
+sealed interface ShortcutCategoryType {
+    data object System : ShortcutCategoryType
+
+    data object MultiTasking : ShortcutCategoryType
+
+    data object InputMethodEditor : ShortcutCategoryType
+
+    data object AppCategories : ShortcutCategoryType
+
+    data class CurrentApp(val packageName: String) : ShortcutCategoryType
 }
 
 data class ShortcutCategory(
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 3b037bc..9e9368d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -16,7 +16,10 @@
 
 package com.android.systemui.keyboard.shortcut.ui.composable
 
+import android.content.Context
+import android.content.pm.PackageManager.NameNotFoundException
 import android.graphics.drawable.Icon
+import android.util.Log
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.Image
@@ -55,6 +58,7 @@
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.NavigationDrawerItemColors
 import androidx.compose.material3.NavigationDrawerItemDefaults
@@ -76,7 +80,6 @@
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
@@ -99,8 +102,10 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.ui.model.IconSource
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.CentralSurfaces
 
 @Composable
 fun ShortcutHelper(
@@ -210,9 +215,9 @@
                 verticalAlignment = Alignment.CenterVertically,
                 modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp)
             ) {
-                Icon(category.icon, contentDescription = null)
+                ShortcutCategoryIcon(category.icon)
                 Spacer(modifier = Modifier.width(16.dp))
-                Text(stringResource(category.labelResId))
+                Text(category.label(LocalContext.current))
                 Spacer(modifier = Modifier.weight(1f))
                 RotatingExpandCollapseIcon(isExpanded)
             }
@@ -221,23 +226,67 @@
     }
 }
 
-private val ShortcutCategory.icon: ImageVector
+private val ShortcutCategory.icon: IconSource
+    @Composable
     get() =
         when (type) {
-            ShortcutCategoryType.SYSTEM -> Icons.Default.Tv
-            ShortcutCategoryType.MULTI_TASKING -> Icons.Default.VerticalSplit
-            ShortcutCategoryType.IME -> Icons.Default.Keyboard
-            ShortcutCategoryType.APP_CATEGORIES -> Icons.Default.Apps
+            ShortcutCategoryType.System -> IconSource(imageVector = Icons.Default.Tv)
+            ShortcutCategoryType.MultiTasking ->
+                IconSource(imageVector = Icons.Default.VerticalSplit)
+            ShortcutCategoryType.InputMethodEditor ->
+                IconSource(imageVector = Icons.Default.Keyboard)
+            ShortcutCategoryType.AppCategories -> IconSource(imageVector = Icons.Default.Apps)
+            is ShortcutCategoryType.CurrentApp -> {
+                val context = LocalContext.current
+                val iconDrawable = context.packageManager.getApplicationIcon(type.packageName)
+                IconSource(painter = rememberDrawablePainter(drawable = iconDrawable))
+            }
         }
 
-private val ShortcutCategory.labelResId: Int
-    get() =
-        when (type) {
-            ShortcutCategoryType.SYSTEM -> R.string.shortcut_helper_category_system
-            ShortcutCategoryType.MULTI_TASKING -> R.string.shortcut_helper_category_multitasking
-            ShortcutCategoryType.IME -> R.string.shortcut_helper_category_input
-            ShortcutCategoryType.APP_CATEGORIES -> R.string.shortcut_helper_category_app_shortcuts
-        }
+@Composable
+fun ShortcutCategoryIcon(
+    source: IconSource,
+    contentDescription: String? = null,
+    modifier: Modifier = Modifier,
+    tint: Color = LocalContentColor.current
+) {
+    if (source.imageVector != null) {
+        Icon(source.imageVector, contentDescription, modifier, tint)
+    } else if (source.painter != null) {
+        Image(source.painter, contentDescription, modifier)
+    }
+}
+
+private fun ShortcutCategory.label(context: Context): String =
+    when (type) {
+        ShortcutCategoryType.System -> context.getString(R.string.shortcut_helper_category_system)
+        ShortcutCategoryType.MultiTasking ->
+            context.getString(R.string.shortcut_helper_category_multitasking)
+        ShortcutCategoryType.InputMethodEditor ->
+            context.getString(R.string.shortcut_helper_category_input)
+        ShortcutCategoryType.AppCategories ->
+            context.getString(R.string.shortcut_helper_category_app_shortcuts)
+        is ShortcutCategoryType.CurrentApp -> getApplicationLabelForCurrentApp(type, context)
+    }
+
+private fun getApplicationLabelForCurrentApp(
+    type: ShortcutCategoryType.CurrentApp,
+    context: Context
+): String {
+    val packageManagerForUser = CentralSurfaces.getPackageManagerForUser(context, context.userId)
+    return try {
+        val currentAppInfo =
+            packageManagerForUser.getApplicationInfoAsUser(
+                type.packageName,
+                /* flags = */ 0,
+                context.userId
+            )
+        packageManagerForUser.getApplicationLabel(currentAppInfo).toString()
+    } catch (e: NameNotFoundException) {
+        Log.wtf(ShortcutHelper.TAG, "Couldn't find app info by package name ${type.packageName}")
+        context.getString(R.string.shortcut_helper_category_current_app_shortcuts)
+    }
+}
 
 @Composable
 private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
@@ -525,8 +574,8 @@
     Column {
         categories.fastForEach {
             CategoryItemTwoPane(
-                label = stringResource(it.labelResId),
-                icon = it.icon,
+                label = it.label(LocalContext.current),
+                iconSource = it.icon,
                 selected = selectedCategory == it.type,
                 onClick = { onCategoryClicked(it) }
             )
@@ -537,7 +586,7 @@
 @Composable
 private fun CategoryItemTwoPane(
     label: String,
-    icon: ImageVector,
+    iconSource: IconSource,
     selected: Boolean,
     onClick: () -> Unit,
     colors: NavigationDrawerItemColors =
@@ -551,9 +600,9 @@
         color = colors.containerColor(selected).value,
     ) {
         Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
-            Icon(
+            ShortcutCategoryIcon(
                 modifier = Modifier.size(24.dp),
-                imageVector = icon,
+                source = iconSource,
                 contentDescription = null,
                 tint = colors.iconColor(selected).value
             )
@@ -649,4 +698,6 @@
     object Dimensions {
         val SinglePaneCategoryCornerRadius = 28.dp
     }
+
+    internal const val TAG = "ShortcutHelperUI"
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt
index 0e4c923..7fc0103 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/model/IconSource.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package com.android.systemui.keyboard.shortcut.ui.model
 
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
 
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+data class IconSource(val imageVector: ImageVector? = null, val painter: Painter? = null)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
index e602cad..ad258f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -19,7 +19,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
 import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -40,8 +39,8 @@
 ) {
 
     val shouldShow =
-        stateInteractor.state
-            .map { it is ShortcutHelperState.Active }
+        categoriesInteractor.shortcutCategories
+            .map { it.isNotEmpty() }
             .distinctUntilChanged()
             .flowOn(backgroundDispatcher)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index fe81b20c..2f872b6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2834,6 +2834,14 @@
      */
     private void handleShow(Bundle options) {
         Trace.beginSection("KeyguardViewMediator#handleShow");
+        try {
+            handleShowInner(options);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    private void handleShowInner(Bundle options) {
         final boolean showUnlocked = options != null
                 && options.getBoolean(OPTION_SHOW_DISMISSIBLE, false);
         final int currentUser = mSelectedUserInteractor.getSelectedUserId();
@@ -2885,8 +2893,6 @@
         mKeyguardDisplayManager.show();
 
         scheduleNonStrongBiometricIdleTimeout();
-
-        Trace.endSection();
     }
 
     /**
@@ -3065,6 +3071,17 @@
             RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
             RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) {
         Trace.beginSection("KeyguardViewMediator#handleStartKeyguardExitAnimation");
+        try {
+            handleStartKeyguardExitAnimationInner(startTime, fadeoutDuration, apps, wallpapers,
+                    nonApps, finishedCallback);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    private void handleStartKeyguardExitAnimationInner(long startTime, long fadeoutDuration,
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) {
         Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime
                 + " fadeoutDuration=" + fadeoutDuration);
         synchronized (KeyguardViewMediator.this) {
@@ -3253,8 +3270,6 @@
                 onKeyguardExitFinished();
             }
         }
-
-        Trace.endSection();
     }
 
     private void onKeyguardExitFinished() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index aee65a8..cd28bec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
+import android.annotation.SuppressLint
+import android.app.DreamManager
 import com.android.app.animation.Interpolators
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -28,6 +31,7 @@
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.kotlin.sample
@@ -53,9 +57,11 @@
     keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     val deviceEntryRepository: DeviceEntryRepository,
     private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor,
+    private val dreamManager: DreamManager,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
@@ -115,6 +121,7 @@
         }
     }
 
+    @SuppressLint("MissingPermission")
     private fun listenForDozingToAny() {
         if (KeyguardWmStateRefactor.isEnabled) {
             return
@@ -126,7 +133,8 @@
                 .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
                 .sample(
                     keyguardInteractor.isKeyguardOccluded,
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     canTransitionToGoneOnWake,
                     keyguardInteractor.primaryBouncerShowing,
                 )
@@ -134,6 +142,7 @@
                     (
                         _,
                         occluded,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         canTransitionToGoneOnWake,
                         primaryBouncerShowing) ->
@@ -163,6 +172,19 @@
                         } else {
                             startTransitionTo(KeyguardState.GLANCEABLE_HUB)
                         }
+                    } else if (
+                        powerInteractor.detailedWakefulness.value.lastWakeReason ==
+                            WakeSleepReason.POWER_BUTTON &&
+                            isCommunalAvailable &&
+                            dreamManager.canStartDreaming(true)
+                    ) {
+                        // This case handles tapping the power button to transition through
+                        // dream -> off -> hub.
+                        if (SceneContainerFlag.isEnabled) {
+                            // TODO(b/336576536): Check if adaptation for scene framework is needed
+                        } else {
+                            startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+                        }
                     } else {
                         startTransitionTo(KeyguardState.LOCKSCREEN)
                     }
@@ -171,6 +193,7 @@
     }
 
     /** Figure out what state to transition to when we awake from DOZING. */
+    @SuppressLint("MissingPermission")
     private fun listenForWakeFromDozing() {
         if (!KeyguardWmStateRefactor.isEnabled) {
             return
@@ -180,7 +203,8 @@
             powerInteractor.detailedWakefulness
                 .filterRelevantKeyguardStateAnd { it.isAwake() }
                 .sample(
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     keyguardInteractor.biometricUnlockState,
                     wakeToGoneInteractor.canWakeDirectlyToGone,
                     keyguardInteractor.primaryBouncerShowing,
@@ -188,6 +212,7 @@
                 .collect {
                     (
                         _,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         biometricUnlockState,
                         canWakeDirectlyToGone,
@@ -227,6 +252,23 @@
                                     ownerReason = "waking from dozing"
                                 )
                             }
+                        } else if (
+                            powerInteractor.detailedWakefulness.value.lastWakeReason ==
+                                WakeSleepReason.POWER_BUTTON &&
+                                isCommunalAvailable &&
+                                dreamManager.canStartDreaming(true)
+                        ) {
+                            // This case handles tapping the power button to transition through
+                            // dream -> off -> hub.
+                            if (SceneContainerFlag.isEnabled) {
+                                // TODO(b/336576536): Check if adaptation for scene framework is
+                                // needed
+                            } else {
+                                startTransitionTo(
+                                    KeyguardState.GLANCEABLE_HUB,
+                                    ownerReason = "waking from dozing"
+                                )
+                            }
                         } else {
                             startTransitionTo(
                                 KeyguardState.LOCKSCREEN,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index ab432d6..c0049d4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.plugins.clocks.ClockId
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
 import com.android.systemui.util.kotlin.combine
@@ -104,21 +103,21 @@
     val clockShouldBeCentered: Flow<Boolean> =
         if (SceneContainerFlag.isEnabled) {
             combine(
-                shadeInteractor.shadeMode,
+                shadeInteractor.isShadeLayoutWide,
                 activeNotificationsInteractor.areAnyNotificationsPresent,
                 keyguardInteractor.isActiveDreamLockscreenHosted,
                 isOnAod,
                 headsUpNotificationInteractor.isHeadsUpOrAnimatingAway,
                 keyguardInteractor.isDozing,
             ) {
-                shadeMode,
+                isShadeLayoutWide,
                 areAnyNotificationsPresent,
                 isActiveDreamLockscreenHosted,
                 isOnAod,
                 isHeadsUp,
                 isDozing ->
                 when {
-                    shadeMode != ShadeMode.Split -> true
+                    !isShadeLayoutWide -> true
                     !areAnyNotificationsPresent -> true
                     isActiveDreamLockscreenHosted -> true
                     // Pulsing notification appears on the right. Move clock left to avoid overlap.
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index de300b2..82b4825 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -28,13 +28,25 @@
      *
      * @property hostPackage the package name of the app that is receiving the content of the media
      *   projection (aka which app the phone screen contents are being sent to).
+     * @property hostDeviceName the name of the other device that's receiving the content of the
+     *   media projection. Null if the media projection is going to this same device (e.g. another
+     *   app is recording the screen).
      */
-    sealed class Projecting(open val hostPackage: String) : MediaProjectionState {
+    sealed class Projecting(
+        open val hostPackage: String,
+        open val hostDeviceName: String?,
+    ) : MediaProjectionState {
         /** The entire screen is being projected. */
-        data class EntireScreen(override val hostPackage: String) : Projecting(hostPackage)
+        data class EntireScreen(
+            override val hostPackage: String,
+            override val hostDeviceName: String? = null,
+        ) : Projecting(hostPackage, hostDeviceName)
 
         /** Only a single task is being projected. */
-        data class SingleTask(override val hostPackage: String, val task: RunningTaskInfo) :
-            Projecting(hostPackage)
+        data class SingleTask(
+            override val hostPackage: String,
+            override val hostDeviceName: String?,
+            val task: RunningTaskInfo,
+        ) : Projecting(hostPackage, hostDeviceName)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 8a9adc7..760ff7d 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.mediaprojection.data.repository
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.hardware.display.DisplayManager
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
 import android.os.Handler
@@ -35,18 +36,21 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
 class MediaProjectionManagerRepository
 @Inject
 constructor(
     private val mediaProjectionManager: MediaProjectionManager,
+    private val displayManager: DisplayManager,
     @Main private val handler: Handler,
     @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -74,12 +78,12 @@
                     object : MediaProjectionManager.Callback() {
                         override fun onStart(info: MediaProjectionInfo?) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onStart")
-                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                            trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
                         }
 
                         override fun onStop(info: MediaProjectionInfo?) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onStop")
-                            trySendWithFailureLogging(MediaProjectionState.NotProjecting, TAG)
+                            trySendWithFailureLogging(CallbackEvent.OnStop, TAG)
                         }
 
                         override fun onRecordingSessionSet(
@@ -87,14 +91,36 @@
                             session: ContentRecordingSession?
                         ) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
-                            launch {
-                                trySendWithFailureLogging(stateForSession(info, session), TAG)
-                            }
+                            trySendWithFailureLogging(
+                                CallbackEvent.OnRecordingSessionSet(info, session),
+                                TAG,
+                            )
                         }
                     }
                 mediaProjectionManager.addCallback(callback, handler)
                 awaitClose { mediaProjectionManager.removeCallback(callback) }
             }
+            // When we get an #onRecordingSessionSet event, we need to do some work in the
+            // background before emitting the right state value. But when we get an #onStop
+            // event, we immediately know what state value to emit.
+            //
+            // Without `mapLatest`, this could be a problem if an #onRecordingSessionSet event
+            // comes in and then an #onStop event comes in shortly afterwards (b/352483752):
+            // 1. #onRecordingSessionSet -> start some work in the background
+            // 2. #onStop -> immediately emit "Not Projecting"
+            // 3. onRecordingSessionSet work finishes -> emit "Projecting"
+            //
+            // At step 3, we *shouldn't* emit "Projecting" because #onStop was the last callback
+            // event we received, so we should be "Not Projecting". This `mapLatest` ensures
+            // that if an #onStop event comes in, we cancel any ongoing work for
+            // #onRecordingSessionSet and we don't emit "Projecting".
+            .mapLatest {
+                when (it) {
+                    is CallbackEvent.OnStart,
+                    is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
+                    is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
+                }
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.Lazily,
@@ -110,14 +136,36 @@
         }
 
         val hostPackage = info.packageName
+        val hostDeviceName =
+            withContext(backgroundDispatcher) {
+                // If the projection is to a different device, then the session's display ID should
+                // identify the display associated with that different device.
+                displayManager.getDisplay(session.virtualDisplayId)?.name
+            }
+
         if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
-            return MediaProjectionState.Projecting.EntireScreen(hostPackage)
+            return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName)
         }
         val matchingTask =
             tasksRepository.findRunningTaskFromWindowContainerToken(
                 checkNotNull(session.tokenToRecord)
-            ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage)
-        return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask)
+            ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage, hostDeviceName)
+        return MediaProjectionState.Projecting.SingleTask(hostPackage, hostDeviceName, matchingTask)
+    }
+
+    /**
+     * Translates [MediaProjectionManager.Callback] events into objects so that we always maintain
+     * the correct callback ordering.
+     */
+    sealed interface CallbackEvent {
+        data object OnStart : CallbackEvent
+
+        data object OnStop : CallbackEvent
+
+        data class OnRecordingSessionSet(
+            val info: MediaProjectionInfo,
+            val session: ContentRecordingSession?,
+        ) : CallbackEvent
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java
index d8c96dd..eadbffe 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java
@@ -41,6 +41,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.people.PeopleBackupFollowUpJob;
 import com.android.systemui.people.SharedPreferencesHelper;
@@ -67,6 +69,7 @@
     private final UserHandle mUserHandle;
     private final PackageManager mPackageManager;
     private final IPeopleManager mIPeopleManager;
+    @Nullable
     private final AppWidgetManager mAppWidgetManager;
 
     /**
@@ -404,6 +407,9 @@
 
     private List<String> getExistingWidgetsForUser(int userId) {
         List<String> existingWidgets = new ArrayList<>();
+        if (mAppWidgetManager == null) {
+            return existingWidgets;
+        }
         int[] ids = mAppWidgetManager.getAppWidgetIds(
                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
         for (int id : ids) {
@@ -491,7 +497,11 @@
 
     /** Sends a broadcast to update the existing Conversation widgets. */
     public static void updateWidgets(Context context) {
-        int[] widgetIds = AppWidgetManager.getInstance(context)
+        AppWidgetManager manager = AppWidgetManager.getInstance(context);
+        if (manager == null) {
+            return;
+        }
+        int[] widgetIds = manager
                 .getAppWidgetIds(new ComponentName(context, PeopleSpaceWidgetProvider.class));
         if (DEBUG) {
             for (int id : widgetIds) {
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
index 0a880293..b0de80c 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
@@ -140,7 +140,7 @@
     private final Object mLock = new Object();
     private final Context mContext;
     private LauncherApps mLauncherApps;
-    private AppWidgetManager mAppWidgetManager;
+    private Optional<AppWidgetManager> mAppWidgetManagerOptional;
     private IPeopleManager mIPeopleManager;
     private SharedPreferences mSharedPrefs;
     private PeopleManager mPeopleManager;
@@ -183,8 +183,9 @@
             };
 
     @Inject
-    public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps,
-            CommonNotifCollection notifCollection,
+    public PeopleSpaceWidgetManager(Context context,
+            Optional<AppWidgetManager> appWidgetManagerOptional,
+            LauncherApps launcherApps, CommonNotifCollection notifCollection,
             PackageManager packageManager, Optional<Bubbles> bubblesOptional,
             UserManager userManager, NotificationManager notificationManager,
             BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor,
@@ -192,7 +193,7 @@
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) {
         if (DEBUG) Log.d(TAG, "constructor");
         mContext = context;
-        mAppWidgetManager = AppWidgetManager.getInstance(context);
+        mAppWidgetManagerOptional = appWidgetManagerOptional;
         mIPeopleManager = IPeopleManager.Stub.asInterface(
                 ServiceManager.getService(Context.PEOPLE_SERVICE));
         mLauncherApps = launcherApps;
@@ -266,14 +267,14 @@
      */
     @VisibleForTesting
     PeopleSpaceWidgetManager(Context context,
-            AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager,
+            Optional<AppWidgetManager> appWidgetManager, IPeopleManager iPeopleManager,
             PeopleManager peopleManager, LauncherApps launcherApps,
             CommonNotifCollection notifCollection, PackageManager packageManager,
             Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
             INotificationManager iNotificationManager, NotificationManager notificationManager,
             @Background Executor executor, UserTracker userTracker) {
         mContext = context;
-        mAppWidgetManager = appWidgetManager;
+        mAppWidgetManagerOptional = appWidgetManager;
         mIPeopleManager = iPeopleManager;
         mPeopleManager = peopleManager;
         mLauncherApps = launcherApps;
@@ -337,6 +338,10 @@
 
     /** Updates the current widget view with provided {@link PeopleSpaceTile}. */
     private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) {
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
         PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId);
         if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString());
 
@@ -349,7 +354,7 @@
 
         // Tell the AppWidgetManager to perform an update on the current app widget.
         if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId);
-        mAppWidgetManager.updateAppWidget(appWidgetId, views);
+        mAppWidgetManagerOptional.get().updateAppWidget(appWidgetId, views);
     }
 
     /** Updates tile in app widget options and the current view. */
@@ -362,13 +367,17 @@
 
     /** Updates tile in app widget options and the current view. */
     public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) {
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
         if (tile == null) {
             Log.w(TAG, "Storing null tile for widget " + appWidgetId);
         }
         synchronized (mTiles) {
             mTiles.put(appWidgetId, tile);
         }
-        Bundle options = mAppWidgetManager.getAppWidgetOptions(appWidgetId);
+        Bundle options = mAppWidgetManagerOptional.get().getAppWidgetOptions(appWidgetId);
         updateAppWidgetViews(appWidgetId, tile, options);
     }
 
@@ -484,6 +493,10 @@
     private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn,
             PeopleSpaceUtils.NotificationAction action,
             Collection<NotificationEntry> notifications) {
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
         try {
             PeopleTileKey key = new PeopleTileKey(
                     sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName());
@@ -491,7 +504,7 @@
                 if (DEBUG) Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString());
                 return;
             }
-            int[] widgetIds = mAppWidgetManager.getAppWidgetIds(
+            int[] widgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds(
                     new ComponentName(mContext, PeopleSpaceWidgetProvider.class)
             );
             if (widgetIds.length == 0) {
@@ -807,10 +820,14 @@
                 UserHandle user,
                 NotificationChannel channel,
                 int modificationType) {
+            if (mAppWidgetManagerOptional.isEmpty()) {
+                return;
+            }
+
             if (channel.isConversation()) {
                 mBgExecutor.execute(() -> {
                     if (mUserManager.isUserUnlocked(user)) {
-                        updateWidgets(mAppWidgetManager.getAppWidgetIds(
+                        updateWidgets(mAppWidgetManagerOptional.get().getAppWidgetIds(
                                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class)
                         ));
                     }
@@ -829,13 +846,18 @@
         // learning about the widget. If so, the widget adder should have populated options with
         // PeopleTileKey arguments.
         if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId);
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
         PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions);
         if (PeopleTileKey.isValid(optionsKey)) {
             if (DEBUG) {
                 Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: "
                         + optionsKey.getShortcutId());
             }
-            AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManager, appWidgetId);
+            AppWidgetOptionsHelper.removePeopleTileKey(mAppWidgetManagerOptional.get(),
+                    appWidgetId);
             addNewWidget(appWidgetId, optionsKey);
         }
         // Update views for new widget dimensions.
@@ -1004,6 +1026,10 @@
     public boolean requestPinAppWidget(ShortcutInfo shortcutInfo, Bundle options) {
         if (DEBUG) Log.d(TAG, "Requesting pin widget, shortcutId: " + shortcutInfo.getId());
 
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return false;
+        }
+
         RemoteViews widgetPreview = getPreview(shortcutInfo.getId(),
                 shortcutInfo.getUserHandle(), shortcutInfo.getPackage(), options);
         if (widgetPreview == null) {
@@ -1017,7 +1043,8 @@
                 PeopleSpaceWidgetPinnedReceiver.getPendingIntent(mContext, shortcutInfo);
 
         ComponentName componentName = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
-        return mAppWidgetManager.requestPinAppWidget(componentName, extras, successCallback);
+        return mAppWidgetManagerOptional.get().requestPinAppWidget(componentName, extras,
+                successCallback);
     }
 
     /** Returns a list of map entries corresponding to user's priority conversations. */
@@ -1104,7 +1131,11 @@
     /** Updates any app widget to the current state, triggered by a broadcast update. */
     @VisibleForTesting
     void updateWidgetsFromBroadcastInBackground(String entryPoint) {
-        int[] appWidgetIds = mAppWidgetManager.getAppWidgetIds(
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
+        int[] appWidgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds(
                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
         if (appWidgetIds == null) {
             return;
@@ -1272,13 +1303,17 @@
         remapSharedFile(widgets);
         remapFollowupFile(widgets);
 
-        int[] widgetIds = mAppWidgetManager.getAppWidgetIds(
+        if (mAppWidgetManagerOptional.isEmpty()) {
+            return;
+        }
+
+        int[] widgetIds = mAppWidgetManagerOptional.get().getAppWidgetIds(
                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
         Bundle b = new Bundle();
         b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true);
         for (int id : widgetIds) {
             if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id);
-            mAppWidgetManager.updateAppWidgetOptions(id, b);
+            mAppWidgetManagerOptional.get().updateAppWidgetOptions(id, b);
         }
 
         updateWidgets(widgetIds);
@@ -1437,14 +1472,15 @@
     @VisibleForTesting
     void updateGeneratedPreviewForUser(UserHandle user) {
         if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier())
-                || !mUserManager.isUserUnlocked(user)) {
+                || !mUserManager.isUserUnlocked(user) || mAppWidgetManagerOptional.isEmpty()) {
             return;
         }
 
         // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI.
         ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
-        List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage(
-                mContext.getPackageName(), user);
+        List<AppWidgetProviderInfo> infos =
+                mAppWidgetManagerOptional.get().getInstalledProvidersForPackage(
+                        mContext.getPackageName(), user);
         if (infos.stream().noneMatch(info -> info.provider.equals(provider))) {
             return;
         }
@@ -1452,7 +1488,7 @@
         if (DEBUG) {
             Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier());
         }
-        boolean success = mAppWidgetManager.setWidgetPreview(
+        boolean success = mAppWidgetManagerOptional.get().setWidgetPreview(
                 provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD,
                 new RemoteViews(mContext.getPackageName(),
                         R.layout.people_space_placeholder_layout));
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 2f87b01..3fdd7f7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -22,7 +22,6 @@
 import android.service.quicksettings.Tile.STATE_ACTIVE
 import android.service.quicksettings.Tile.STATE_INACTIVE
 import android.text.TextUtils
-import android.util.Log
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
 import androidx.compose.animation.graphics.res.animatedVectorResource
@@ -465,7 +464,6 @@
     animateToEnd: Boolean = false,
     modifier: Modifier = Modifier,
 ) {
-    Log.d("Fabian", "Recomposing tile icon")
     val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
     val context = LocalContext.current
     val loadedDrawable =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index f34389e..53594bb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -20,12 +20,14 @@
 /** Return the subtitle resource Id of the given tile. */
 object SubtitleArrayMapping {
     private val subtitleIdsMap: HashMap<String, Int> = HashMap()
+
     init {
         subtitleIdsMap["internet"] = R.array.tile_states_internet
         subtitleIdsMap["wifi"] = R.array.tile_states_wifi
         subtitleIdsMap["cell"] = R.array.tile_states_cell
         subtitleIdsMap["battery"] = R.array.tile_states_battery
         subtitleIdsMap["dnd"] = R.array.tile_states_dnd
+        subtitleIdsMap["modes"] = R.array.tile_states_modes
         subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight
         subtitleIdsMap["rotation"] = R.array.tile_states_rotation
         subtitleIdsMap["bt"] = R.array.tile_states_bt
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index c971f54..edc49cac2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -183,9 +183,9 @@
             Context context,
             InternetDialogManager internetDialogManager,
             InternetDialogController internetDialogController,
-            @Assisted(ABOVE_STATUS_BAR) boolean canConfigMobileData,
-            @Assisted(CAN_CONFIG_MOBILE_DATA) boolean canConfigWifi,
-            @Assisted(CAN_CONFIG_WIFI) boolean aboveStatusBar,
+            @Assisted(CAN_CONFIG_MOBILE_DATA) boolean canConfigMobileData,
+            @Assisted(CAN_CONFIG_WIFI) boolean canConfigWifi,
+            @Assisted(ABOVE_STATUS_BAR) boolean aboveStatusBar,
             @Assisted CoroutineScope coroutineScope,
             UiEventLogger uiEventLogger,
             DialogTransitionAnimator dialogTransitionAnimator,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
new file mode 100644
index 0000000..da4d2f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.app.Flags
+import android.os.UserHandle
+import android.provider.Settings.Global.ZEN_MODE_OFF
+import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenModeRepository) :
+    QSTileDataInteractor<ModesTileModel> {
+    // TODO(b/346519570): This should be checking for any enabled modes.
+    private val zenModeActive =
+        zenModeRepository.globalZenMode.map { it != ZEN_MODE_OFF }.distinctUntilChanged()
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<ModesTileModel> {
+        return zenModeActive.map { ModesTileModel(isActivated = it) }
+    }
+
+    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
new file mode 100644
index 0000000..e2fea84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+class ModesTileUserActionInteractor @Inject constructor() :
+    QSTileUserActionInteractor<ModesTileModel> {
+    override suspend fun handleInput(input: QSTileInput<ModesTileModel>) {
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    // TODO(b/346519570) open dialog
+                }
+                is QSTileUserAction.LongClick -> {
+                    // TODO(b/346519570) open settings
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
index 0e4c923..e44413a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
@@ -14,11 +14,5 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+package com.android.systemui.qs.tiles.impl.modes.domain.model
+data class ModesTileModel(val isActivated: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
new file mode 100644
index 0000000..07b393e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.ui
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class ModesTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    val theme: Resources.Theme,
+) : QSTileDataToStateMapper<ModesTileModel> {
+    override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            val iconRes =
+                if (data.isActivated) {
+                    R.drawable.qs_dnd_icon_on
+                } else {
+                    R.drawable.qs_dnd_icon_off
+                }
+            val icon = Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null)
+            this.icon = { icon }
+            if (data.isActivated) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                secondaryLabel = "Some modes enabled idk" // TODO(b/346519570)
+            } else {
+                activationState = QSTileState.ActivationState.INACTIVE
+                secondaryLabel = "Off" // TODO(b/346519570)
+            }
+            contentDescription = label
+            supportedActions =
+                setOf(
+                    QSTileState.UserAction.CLICK,
+                    QSTileState.UserAction.LONG_CLICK,
+                )
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
index 1868b4a..54e0319 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
@@ -40,7 +40,7 @@
     private val intentExecutor: ActionIntentExecutor,
     @Application private val applicationScope: CoroutineScope,
     @Assisted val window: Window,
-    @Assisted val viewProxy: ScreenshotViewProxy,
+    @Assisted val viewProxy: ScreenshotShelfViewProxy,
     @Assisted val finishDismiss: () -> Unit,
 ) {
 
@@ -109,7 +109,7 @@
     interface Factory {
         fun create(
             window: Window,
-            viewProxy: ScreenshotViewProxy,
+            viewProxy: ScreenshotShelfViewProxy,
             finishDismiss: (() -> Unit)
         ): ActionExecutor
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
deleted file mode 100644
index 3d024a6..0000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.screenshot
-
-import android.animation.Animator
-import android.app.Notification
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Rect
-import android.util.Log
-import android.view.KeyEvent
-import android.view.LayoutInflater
-import android.view.ScrollCaptureResponse
-import android.view.View
-import android.view.ViewTreeObserver
-import android.view.WindowInsets
-import android.window.OnBackInvokedCallback
-import android.window.OnBackInvokedDispatcher
-import androidx.appcompat.content.res.AppCompatResources
-import com.android.internal.logging.UiEventLogger
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.res.R
-import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
-import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
-import com.android.systemui.screenshot.scroll.ScrollCaptureController
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-
-/**
- * Legacy implementation of screenshot view methods. Just proxies the calls down into the original
- * ScreenshotView.
- */
-class LegacyScreenshotViewProxy
-@AssistedInject
-constructor(
-    private val logger: UiEventLogger,
-    flags: FeatureFlags,
-    @Assisted private val context: Context,
-    @Assisted private val displayId: Int
-) : ScreenshotViewProxy {
-    override val view: ScreenshotView =
-        LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView
-    override val screenshotPreview: View
-    override var packageName: String = ""
-        set(value) {
-            field = value
-            view.setPackageName(value)
-        }
-    override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
-        set(value) {
-            field = value
-            view.setCallbacks(value)
-        }
-    override var screenshot: ScreenshotData? = null
-        set(value) {
-            field = value
-            value?.let {
-                val badgeBg =
-                    AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background)
-                val user = it.userHandle
-                if (badgeBg != null && user != null) {
-                    view.badgeScreenshot(context.packageManager.getUserBadgedIcon(badgeBg, user))
-                }
-                view.setScreenshot(it)
-            }
-        }
-
-    override val isAttachedToWindow
-        get() = view.isAttachedToWindow
-    override val isDismissing
-        get() = view.isDismissing
-    override val isPendingSharedTransition
-        get() = view.isPendingSharedTransition
-
-    init {
-        view.setUiEventLogger(logger)
-        view.setDefaultDisplay(displayId)
-        view.setFlags(flags)
-        addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
-        setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
-        if (LogConfig.DEBUG_WINDOW) {
-            Log.d(TAG, "adding OnComputeInternalInsetsListener")
-        }
-        view.viewTreeObserver.addOnComputeInternalInsetsListener(view)
-        screenshotPreview = view.screenshotPreview
-    }
-
-    override fun reset() = view.reset()
-    override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets)
-    override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets)
-
-    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator =
-        view.createScreenshotDropInAnimation(screenRect, showFlash)
-
-    override fun addQuickShareChip(quickShareAction: Notification.Action) =
-        view.addQuickShareChip(quickShareAction)
-
-    override fun setChipIntents(imageData: ScreenshotController.SavedImageData) =
-        view.setChipIntents(imageData)
-
-    override fun requestDismissal(event: ScreenshotEvent?) {
-        if (DEBUG_DISMISS) {
-            Log.d(TAG, "screenshot dismissal requested")
-        }
-        // If we're already animating out, don't restart the animation
-        if (view.isDismissing) {
-            if (DEBUG_DISMISS) {
-                Log.v(TAG, "Already dismissing, ignoring duplicate command $event")
-            }
-            return
-        }
-        event?.let { logger.log(event, 0, packageName) }
-        view.animateDismissal()
-    }
-
-    override fun showScrollChip(packageName: String, onClick: Runnable) =
-        view.showScrollChip(packageName, onClick)
-
-    override fun hideScrollChip() = view.hideScrollChip()
-
-    override fun prepareScrollingTransition(
-        response: ScrollCaptureResponse,
-        screenBitmap: Bitmap,
-        newScreenshot: Bitmap,
-        screenshotTakenInPortrait: Boolean,
-        onTransitionPrepared: Runnable,
-    ) {
-        view.prepareScrollingTransition(
-            response,
-            screenBitmap,
-            newScreenshot,
-            screenshotTakenInPortrait
-        )
-        view.post { onTransitionPrepared.run() }
-    }
-
-    override fun startLongScreenshotTransition(
-        transitionDestination: Rect,
-        onTransitionEnd: Runnable,
-        longScreenshot: ScrollCaptureController.LongScreenshot
-    ) = view.startLongScreenshotTransition(transitionDestination, onTransitionEnd, longScreenshot)
-
-    override fun restoreNonScrollingUi() = view.restoreNonScrollingUi()
-
-    override fun fadeForSharedTransition() {} // unused
-
-    override fun stopInputListening() = view.stopInputListening()
-
-    override fun requestFocus() {
-        view.requestFocus()
-    }
-
-    override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
-
-    override fun prepareEntranceAnimation(runnable: Runnable) {
-        view.viewTreeObserver.addOnPreDrawListener(
-            object : ViewTreeObserver.OnPreDrawListener {
-                override fun onPreDraw(): Boolean {
-                    if (LogConfig.DEBUG_WINDOW) {
-                        Log.d(TAG, "onPreDraw: startAnimation")
-                    }
-                    view.viewTreeObserver.removeOnPreDrawListener(this)
-                    runnable.run()
-                    return true
-                }
-            }
-        )
-    }
-
-    private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
-        val onBackInvokedCallback = OnBackInvokedCallback {
-            if (LogConfig.DEBUG_INPUT) {
-                Log.d(TAG, "Predictive Back callback dispatched")
-            }
-            onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
-        }
-        view.addOnAttachStateChangeListener(
-            object : View.OnAttachStateChangeListener {
-                override fun onViewAttachedToWindow(v: View) {
-                    if (LogConfig.DEBUG_INPUT) {
-                        Log.d(TAG, "Registering Predictive Back callback")
-                    }
-                    view
-                        .findOnBackInvokedDispatcher()
-                        ?.registerOnBackInvokedCallback(
-                            OnBackInvokedDispatcher.PRIORITY_DEFAULT,
-                            onBackInvokedCallback
-                        )
-                }
-
-                override fun onViewDetachedFromWindow(view: View) {
-                    if (LogConfig.DEBUG_INPUT) {
-                        Log.d(TAG, "Unregistering Predictive Back callback")
-                    }
-                    view
-                        .findOnBackInvokedDispatcher()
-                        ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
-                }
-            }
-        )
-    }
-    private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
-        view.setOnKeyListener(
-            object : View.OnKeyListener {
-                override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
-                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
-                        if (LogConfig.DEBUG_INPUT) {
-                            Log.d(TAG, "onKeyEvent: $keyCode")
-                        }
-                        onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
-                        return true
-                    }
-                    return false
-                }
-            }
-        )
-    }
-
-    @AssistedFactory
-    interface Factory : ScreenshotViewProxy.Factory {
-        override fun getProxy(context: Context, displayId: Int): LegacyScreenshotViewProxy
-    }
-
-    companion object {
-        private const val TAG = "LegacyScreenshotViewProxy"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
index 5960462..474afa8b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -137,7 +137,7 @@
         val offset = container.height + params.topMargin + params.bottomMargin
         val anim = if (animateIn) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f)
         with(anim) {
-            duration = ScreenshotView.SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS
+            duration = MESSAGE_EXPANSION_DURATION_MS
             interpolator = AccelerateDecelerateInterpolator()
             addUpdateListener { valueAnimator: ValueAnimator ->
                 val interpolation = valueAnimator.animatedValue as Float
@@ -147,4 +147,8 @@
         }
         return anim
     }
+
+    companion object {
+        const val MESSAGE_EXPANSION_DURATION_MS: Long = 400
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 95ee2e0..7739009 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
 import static com.android.systemui.Flags.screenshotPrivateProfileAccessibilityAnnouncementFix;
+import static com.android.systemui.Flags.screenshotSaveImageExporter;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -190,7 +191,7 @@
 
     private final WindowContext mContext;
     private final FeatureFlags mFlags;
-    private final ScreenshotViewProxy mViewProxy;
+    private final ScreenshotShelfViewProxy mViewProxy;
     private final ScreenshotNotificationsController mNotificationsController;
     private final ScreenshotSmartActions mScreenshotSmartActions;
     private final UiEventLogger mUiEventLogger;
@@ -230,13 +231,6 @@
     private String mPackageName = "";
     private final BroadcastReceiver mCopyBroadcastReceiver;
 
-    // When false, the screenshot is taken without showing the ui. Note that this only applies to
-    // external displays, as on the default one the UI should **always** be shown.
-    // This is needed in case of screenshot during display mirroring, as adding another window to
-    // the external display makes mirroring stop.
-    // When there is a way to distinguish between displays that are mirroring or extending, this
-    // can be removed and we can directly show the ui only in the extended case.
-    private final Boolean mShowUIOnExternalDisplay;
     /** Tracks config changes that require re-creating UI */
     private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
             ActivityInfo.CONFIG_ORIENTATION
@@ -252,7 +246,7 @@
             Context context,
             WindowManager windowManager,
             FeatureFlags flags,
-            ScreenshotViewProxy.Factory viewProxyFactory,
+            ScreenshotShelfViewProxy.Factory viewProxyFactory,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
             UiEventLogger uiEventLogger,
@@ -272,8 +266,7 @@
             MessageContainerController messageContainerController,
             Provider<ScreenshotSoundController> screenshotSoundController,
             AnnouncementResolver announcementResolver,
-            @Assisted Display display,
-            @Assisted boolean showUIOnExternalDisplay
+            @Assisted Display display
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
         mNotificationsController = screenshotNotificationsControllerFactory.create(
@@ -347,7 +340,6 @@
         mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
                         ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null,
                 Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION);
-        mShowUIOnExternalDisplay = showUIOnExternalDisplay;
     }
 
     @Override
@@ -381,7 +373,7 @@
             Log.w(TAG, "User setup not complete, displaying toast only");
             // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
             // and sharing shouldn't be exposed to the user.
-            saveScreenshotAndToast(screenshot.getUserHandle(), finisher);
+            saveScreenshotAndToast(screenshot, finisher);
             return;
         }
 
@@ -397,17 +389,15 @@
 
         prepareViewForNewScreenshot(screenshot, oldPackageName);
 
-        if (!shouldShowUi()) {
-            saveScreenshotInWorkerThread(
-                    screenshot.getUserHandle(), finisher, this::logSuccessOnActionsReady,
-                    (ignored) -> {
-                    });
-            return;
-        }
-
         final UUID requestId;
         requestId = mActionsController.setCurrentScreenshot(screenshot);
-        saveScreenshotInBackground(screenshot, requestId, finisher);
+        saveScreenshotInBackground(screenshot, requestId, finisher, result -> {
+            if (result.uri != null) {
+                ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult(
+                        result.uri, screenshot.getUserOrDefault(), result.timestamp);
+                mActionsController.setCompletedScreenshot(requestId, savedScreenshot);
+            }
+        });
 
         if (screenshot.getTaskId() >= 0) {
             mAssistContentRequester.requestAssistContent(
@@ -453,10 +443,6 @@
                 (v, insets) -> WindowInsets.CONSUMED);
     }
 
-    private boolean shouldShowUi() {
-        return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
-    }
-
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
         withWindowAttached(() -> {
             if (screenshotPrivateProfileAccessibilityAnnouncementFix()) {
@@ -491,9 +477,6 @@
         }
 
         mViewProxy.setPackageName(mPackageName);
-
-        mViewProxy.updateOrientation(
-                mWindowManager.getCurrentWindowMetrics().getWindowInsets());
     }
 
     /**
@@ -542,7 +525,7 @@
         }
 
         mMessageContainerController.setView(mViewProxy.getView());
-        mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() {
+        mViewProxy.setCallbacks(new ScreenshotShelfViewProxy.ScreenshotViewCallback() {
             @Override
             public void onUserInteraction() {
                 if (DEBUG_INPUT) {
@@ -552,13 +535,6 @@
             }
 
             @Override
-            public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) {
-                Pair<ActivityOptions, ExitTransitionCoordinator> exit = createWindowTransition();
-                mActionIntentExecutor.launchIntentAsync(
-                        intent, owner, overrideTransition, exit.first, exit.second);
-            }
-
-            @Override
             public void onDismiss() {
                 finishDismiss();
             }
@@ -724,29 +700,40 @@
      * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
      * failure).
      */
-    private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
+    private void saveScreenshotAndToast(ScreenshotData screenshot, Consumer<Uri> finisher) {
         // Play the shutter sound to notify that we've taken a screenshot
         playCameraSoundIfNeeded();
 
-        saveScreenshotInWorkerThread(
-                owner,
-                /* onComplete */ finisher,
-                /* actionsReadyListener */ imageData -> {
-                    if (DEBUG_CALLBACK) {
-                        Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri);
-                    }
-                    finisher.accept(imageData.uri);
-                    if (imageData.uri == null) {
-                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
-                        mNotificationsController.notifyScreenshotError(
-                                R.string.screenshot_failed_to_save_text);
-                    } else {
-                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
-                        mScreenshotHandler.post(() -> Toast.makeText(mContext,
-                                R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
-                    }
-                },
-                null);
+        if (screenshotSaveImageExporter()) {
+            saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
+                if (result.uri != null) {
+                    mScreenshotHandler.post(() -> Toast.makeText(mContext,
+                            R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
+                }
+            });
+        } else {
+            saveScreenshotInWorkerThread(
+                    screenshot.getUserHandle(),
+                    /* onComplete */ finisher,
+                    /* actionsReadyListener */ imageData -> {
+                        if (DEBUG_CALLBACK) {
+                            Log.d(TAG,
+                                    "returning URI to finisher (Consumer<URI>): " + imageData.uri);
+                        }
+                        finisher.accept(imageData.uri);
+                        if (imageData.uri == null) {
+                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0,
+                                    mPackageName);
+                            mNotificationsController.notifyScreenshotError(
+                                    R.string.screenshot_failed_to_save_text);
+                        } else {
+                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
+                            mScreenshotHandler.post(() -> Toast.makeText(mContext,
+                                    R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
+                        }
+                    },
+                    null);
+        }
     }
 
     /**
@@ -819,8 +806,8 @@
         mScreenshotHandler.cancelTimeout();
     }
 
-    private void saveScreenshotInBackground(
-            ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
+    private void saveScreenshotInBackground(ScreenshotData screenshot, UUID requestId,
+            Consumer<Uri> finisher, Consumer<ImageExporter.Result> onResult) {
         ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
                 requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(),
                 mDisplay.getDisplayId());
@@ -829,10 +816,7 @@
                 ImageExporter.Result result = future.get();
                 Log.d(TAG, "Saved screenshot: " + result);
                 logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
-                if (result.uri != null) {
-                    mActionsController.setCompletedScreenshot(requestId, new ScreenshotSavedResult(
-                            result.uri, screenshot.getUserOrDefault(), result.timestamp));
-                }
+                onResult.accept(result);
                 if (DEBUG_CALLBACK) {
                     Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
                             + "finisher.accept(\"" + result.uri + "\"");
@@ -877,62 +861,6 @@
         mSaveInBgTask.execute();
     }
 
-
-    /**
-     * Sets up the action shade and its entrance animation, once we get the screenshot URI.
-     */
-    private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) {
-        logSuccessOnActionsReady(imageData);
-        mScreenshotHandler.resetTimeout();
-
-        if (imageData.uri != null) {
-            if (DEBUG_UI) {
-                Log.d(TAG, "Showing UI actions");
-            }
-            if (!imageData.owner.equals(Process.myUserHandle())) {
-                Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as "
-                        + imageData.uri);
-            }
-            mScreenshotHandler.post(() -> {
-                if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
-                    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            super.onAnimationEnd(animation);
-                            mViewProxy.setChipIntents(imageData);
-                        }
-                    });
-                } else {
-                    mViewProxy.setChipIntents(imageData);
-                }
-            });
-        }
-    }
-
-    /**
-     * Sets up the action shade and its entrance animation, once we get the Quick Share action data.
-     */
-    private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) {
-        if (DEBUG_UI) {
-            Log.d(TAG, "Showing UI for Quick Share action");
-        }
-        if (quickShareData.quickShareAction != null) {
-            mScreenshotHandler.post(() -> {
-                if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
-                    mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            super.onAnimationEnd(animation);
-                            mViewProxy.addQuickShareChip(quickShareData.quickShareAction);
-                        }
-                    });
-                } else {
-                    mViewProxy.addQuickShareChip(quickShareData.quickShareAction);
-                }
-            });
-        }
-    }
-
     /**
      * Logs success/failure of the screenshot saving task, and shows an error if it failed.
      */
@@ -1028,9 +956,7 @@
          * Creates an instance of the controller for that specific display.
          *
          * @param display                 display to capture
-         * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external
-         *                                display.
          */
-        ScreenshotController create(Display display, boolean showUIOnExternalDisplay);
+        ScreenshotController create(Display display);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 1b5fa34..50215af 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -18,7 +18,6 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
-import android.app.Notification
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Rect
@@ -45,7 +44,6 @@
 import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
 import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
 import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
-import com.android.systemui.screenshot.ScreenshotController.SavedImageData
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
 import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import com.android.systemui.screenshot.ui.ScreenshotAnimationController
@@ -70,13 +68,23 @@
     private val thumbnailObserver: ThumbnailObserver,
     @Assisted private val context: Context,
     @Assisted private val displayId: Int
-) : ScreenshotViewProxy {
-    override val view: ScreenshotShelfView =
+) {
+
+    interface ScreenshotViewCallback {
+        fun onUserInteraction()
+
+        fun onDismiss()
+
+        /** DOWN motion event was observed outside of the touchable areas of this view. */
+        fun onTouchOutside()
+    }
+
+    val view: ScreenshotShelfView =
         LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
-    override val screenshotPreview: View
-    override var packageName: String = ""
-    override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
-    override var screenshot: ScreenshotData? = null
+    val screenshotPreview: View
+    var packageName: String = ""
+    var callbacks: ScreenshotViewCallback? = null
+    var screenshot: ScreenshotData? = null
         set(value) {
             value?.let {
                 viewModel.setScreenshotBitmap(it.bitmap)
@@ -92,10 +100,11 @@
             field = value
         }
 
-    override val isAttachedToWindow
+    val isAttachedToWindow
         get() = view.isAttachedToWindow
-    override var isDismissing = false
-    override var isPendingSharedTransition = false
+
+    var isDismissing = false
+    var isPendingSharedTransition = false
 
     private val animationController = ScreenshotAnimationController(view, viewModel)
     private var inputMonitor: InputMonitorCompat? = null
@@ -136,17 +145,17 @@
         )
     }
 
-    override fun reset() {
+    fun reset() {
         animationController.cancel()
         isPendingSharedTransition = false
         viewModel.reset()
     }
-    override fun updateInsets(insets: WindowInsets) {
+
+    fun updateInsets(insets: WindowInsets) {
         view.updateInsets(insets)
     }
-    override fun updateOrientation(insets: WindowInsets) {}
 
-    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
+    fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
         val entrance =
             animationController.getEntranceAnimation(screenRect, showFlash) {
                 viewModel.setAnimationState(AnimationState.ENTRANCE_REVEAL)
@@ -164,11 +173,7 @@
         return entrance
     }
 
-    override fun addQuickShareChip(quickShareAction: Notification.Action) {}
-
-    override fun setChipIntents(imageData: SavedImageData) {}
-
-    override fun requestDismissal(event: ScreenshotEvent?) {
+    fun requestDismissal(event: ScreenshotEvent?) {
         requestDismissal(event, null)
     }
 
@@ -187,6 +192,7 @@
                 override fun onAnimationStart(animator: Animator) {
                     isDismissing = true
                 }
+
                 override fun onAnimationEnd(animator: Animator) {
                     isDismissing = false
                     callbacks?.onDismiss()
@@ -196,11 +202,7 @@
         animator.start()
     }
 
-    override fun showScrollChip(packageName: String, onClick: Runnable) {}
-
-    override fun hideScrollChip() {}
-
-    override fun prepareScrollingTransition(
+    fun prepareScrollingTransition(
         response: ScrollCaptureResponse,
         screenBitmap: Bitmap, // unused
         newScreenshot: Bitmap,
@@ -228,7 +230,7 @@
         return r
     }
 
-    override fun startLongScreenshotTransition(
+    fun startLongScreenshotTransition(
         transitionDestination: Rect,
         onTransitionEnd: Runnable,
         longScreenshot: ScrollCaptureController.LongScreenshot,
@@ -243,27 +245,27 @@
         transitionAnimation.start()
     }
 
-    override fun restoreNonScrollingUi() {
+    fun restoreNonScrollingUi() {
         viewModel.setScrollableRect(null)
         viewModel.setScrollingScrimBitmap(null)
         animationController.restoreUI()
         callbacks?.onUserInteraction() // reset the timeout
     }
 
-    override fun stopInputListening() {
+    fun stopInputListening() {
         inputMonitor?.dispose()
         inputMonitor = null
         inputEventReceiver?.dispose()
         inputEventReceiver = null
     }
 
-    override fun requestFocus() {
+    fun requestFocus() {
         view.requestFocus()
     }
 
-    override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+    fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
 
-    override fun prepareEntranceAnimation(runnable: Runnable) {
+    fun prepareEntranceAnimation(runnable: Runnable) {
         view.viewTreeObserver.addOnPreDrawListener(
             object : ViewTreeObserver.OnPreDrawListener {
                 override fun onPreDraw(): Boolean {
@@ -276,7 +278,7 @@
         )
     }
 
-    override fun fadeForSharedTransition() {
+    fun fadeForSharedTransition() {
         animationController.fadeForSharedTransition()
     }
 
@@ -349,7 +351,7 @@
     }
 
     @AssistedFactory
-    interface Factory : ScreenshotViewProxy.Factory {
-        override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+    interface Factory {
+        fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
deleted file mode 100644
index 59e38a8..0000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ /dev/null
@@ -1,1126 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.screenshot;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_TAKE_SCREENSHOT;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
-import static com.android.systemui.screenshot.LogConfig.logTag;
-import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS;
-
-import static java.util.Objects.requireNonNull;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.BroadcastOptions;
-import android.app.Notification;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BlendMode;
-import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Bundle;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.MathUtils;
-import android.view.Choreographer;
-import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.GestureDetector;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.ScrollCaptureResponse;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import androidx.constraintlayout.widget.ConstraintLayout;
-
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.res.R;
-import com.android.systemui.screenshot.scroll.ScrollCaptureController;
-import com.android.systemui.shared.system.InputChannelCompat;
-import com.android.systemui.shared.system.InputMonitorCompat;
-import com.android.systemui.shared.system.QuickStepContract;
-
-import java.util.ArrayList;
-
-/**
- * Handles the visual elements and animations for the screenshot flow.
- */
-public class ScreenshotView extends FrameLayout implements
-        ViewTreeObserver.OnComputeInternalInsetsListener {
-
-    public interface ScreenshotViewCallback {
-        void onUserInteraction();
-
-        void onAction(Intent intent, UserHandle owner, boolean overrideTransition);
-
-        void onDismiss();
-
-        /** DOWN motion event was observed outside of the touchable areas of this view. */
-        void onTouchOutside();
-    }
-
-    private static final String TAG = logTag(ScreenshotView.class);
-
-    private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
-    private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217;
-    // delay before starting to fade in dismiss button
-    private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200;
-    private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
-    private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
-    private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
-    public static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
-    private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
-    private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
-
-    private final Resources mResources;
-    private final Interpolator mFastOutSlowIn;
-    private final DisplayMetrics mDisplayMetrics;
-    private final float mFixedSize;
-    private final AccessibilityManager mAccessibilityManager;
-    private final GestureDetector mSwipeDetector;
-
-    private int mDefaultDisplay = Display.DEFAULT_DISPLAY;
-    private int mNavMode;
-    private boolean mOrientationPortrait;
-    private boolean mDirectionLTR;
-
-    private ImageView mScrollingScrim;
-    private DraggableConstraintLayout mScreenshotStatic;
-    private ImageView mScreenshotPreview;
-    private ImageView mScreenshotBadge;
-    private View mScreenshotPreviewBorder;
-    private ImageView mScrollablePreview;
-    private ImageView mScreenshotFlash;
-    private ImageView mActionsContainerBackground;
-    private HorizontalScrollView mActionsContainer;
-    private LinearLayout mActionsView;
-    private FrameLayout mDismissButton;
-    private OverlayActionChip mShareChip;
-    private OverlayActionChip mEditChip;
-    private OverlayActionChip mScrollChip;
-    private OverlayActionChip mQuickShareChip;
-
-    private UiEventLogger mUiEventLogger;
-    private ScreenshotViewCallback mCallbacks;
-    private boolean mPendingSharedTransition;
-    private InputMonitorCompat mInputMonitor;
-    private InputChannelCompat.InputEventReceiver mInputEventReceiver;
-    private boolean mShowScrollablePreview;
-    private String mPackageName = "";
-
-    private final ArrayList<OverlayActionChip> mSmartChips = new ArrayList<>();
-    private PendingInteraction mPendingInteraction;
-    // Should only be set/used if the SCREENSHOT_METADATA flag is set.
-    private ScreenshotData mScreenshotData;
-
-    private final InteractionJankMonitor mInteractionJankMonitor;
-    private FeatureFlags mFlags;
-    private final Bundle mInteractiveBroadcastOption;
-
-    private enum PendingInteraction {
-        PREVIEW,
-        EDIT,
-        SHARE,
-        QUICK_SHARE
-    }
-
-    public ScreenshotView(Context context) {
-        this(context, null);
-    }
-
-    public ScreenshotView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public ScreenshotView(
-            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mResources = mContext.getResources();
-        mInteractionJankMonitor = getInteractionJankMonitorInstance();
-
-        BroadcastOptions options = BroadcastOptions.makeBasic();
-        options.setInteractive(true);
-        mInteractiveBroadcastOption = options.toBundle();
-
-        mFixedSize = mResources.getDimensionPixelSize(R.dimen.overlay_x_scale);
-
-        // standard material ease
-        mFastOutSlowIn =
-                AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in);
-
-        mDisplayMetrics = new DisplayMetrics();
-        mContext.getDisplay().getRealMetrics(mDisplayMetrics);
-
-        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
-
-        mSwipeDetector = new GestureDetector(mContext,
-                new GestureDetector.SimpleOnGestureListener() {
-                    final Rect mActionsRect = new Rect();
-
-                    @Override
-                    public boolean onScroll(
-                            MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
-                        mActionsContainer.getBoundsOnScreen(mActionsRect);
-                        // return true if we aren't in the actions bar, or if we are but it isn't
-                        // scrollable in the direction of movement
-                        return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
-                                || !mActionsContainer.canScrollHorizontally((int) distanceX);
-                    }
-                });
-        mSwipeDetector.setIsLongpressEnabled(false);
-        addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
-            @Override
-            public void onViewAttachedToWindow(View v) {
-                startInputListening();
-            }
-
-            @Override
-            public void onViewDetachedFromWindow(View v) {
-                stopInputListening();
-            }
-        });
-    }
-
-    private InteractionJankMonitor getInteractionJankMonitorInstance() {
-        return InteractionJankMonitor.getInstance();
-    }
-
-    public void hideScrollChip() {
-        mScrollChip.setVisibility(View.GONE);
-    }
-
-    /**
-     * Called to display the scroll action chip when support is detected.
-     *
-     * @param packageName the owning package of the window to be captured
-     * @param onClick     the action to take when the chip is clicked.
-     */
-    public void showScrollChip(String packageName, Runnable onClick) {
-        if (DEBUG_SCROLL) {
-            Log.d(TAG, "Showing Scroll option");
-        }
-        mScrollChip.setVisibility(VISIBLE);
-        mScrollChip.setOnClickListener((v) -> onClick.run());
-    }
-
-    @Override // ViewTreeObserver.OnComputeInternalInsetsListener
-    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
-        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
-        inoutInfo.touchableRegion.set(getTouchRegion(true));
-    }
-
-    private Region getSwipeRegion() {
-        Region swipeRegion = new Region();
-
-        final Rect tmpRect = new Rect();
-        int swipePadding = (int) FloatingWindowUtil.dpToPx(
-                mDisplayMetrics, DraggableConstraintLayout.SWIPE_PADDING_DP * -1);
-        mScreenshotPreview.getBoundsOnScreen(tmpRect);
-        tmpRect.inset(swipePadding, swipePadding);
-        swipeRegion.op(tmpRect, Region.Op.UNION);
-        mActionsContainerBackground.getBoundsOnScreen(tmpRect);
-        tmpRect.inset(swipePadding, swipePadding);
-        swipeRegion.op(tmpRect, Region.Op.UNION);
-        mDismissButton.getBoundsOnScreen(tmpRect);
-        swipeRegion.op(tmpRect, Region.Op.UNION);
-
-        View messageContainer = findViewById(R.id.screenshot_message_container);
-        if (messageContainer != null) {
-            messageContainer.getBoundsOnScreen(tmpRect);
-            swipeRegion.op(tmpRect, Region.Op.UNION);
-        }
-        View messageDismiss = findViewById(R.id.message_dismiss_button);
-        if (messageDismiss != null) {
-            messageDismiss.getBoundsOnScreen(tmpRect);
-            swipeRegion.op(tmpRect, Region.Op.UNION);
-        }
-
-        return swipeRegion;
-    }
-
-    private Region getTouchRegion(boolean includeScrim) {
-        Region touchRegion = getSwipeRegion();
-
-        if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) {
-            final Rect tmpRect = new Rect();
-            mScrollingScrim.getBoundsOnScreen(tmpRect);
-            touchRegion.op(tmpRect, Region.Op.UNION);
-        }
-
-        if (QuickStepContract.isGesturalMode(mNavMode)) {
-            final WindowManager wm = mContext.getSystemService(WindowManager.class);
-            final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
-            final Insets gestureInsets = windowMetrics.getWindowInsets().getInsets(
-                    WindowInsets.Type.systemGestures());
-            // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE
-            Rect inset = new Rect(0, 0, gestureInsets.left, mDisplayMetrics.heightPixels);
-            touchRegion.op(inset, Region.Op.UNION);
-            inset.set(mDisplayMetrics.widthPixels - gestureInsets.right, 0,
-                    mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels);
-            touchRegion.op(inset, Region.Op.UNION);
-        }
-        return touchRegion;
-    }
-
-    private void startInputListening() {
-        stopInputListening();
-        mInputMonitor = new InputMonitorCompat("Screenshot", mDefaultDisplay);
-        mInputEventReceiver = mInputMonitor.getInputReceiver(
-                Looper.getMainLooper(), Choreographer.getInstance(), ev -> {
-                    if (ev instanceof MotionEvent) {
-                        MotionEvent event = (MotionEvent) ev;
-                        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
-                                && !getTouchRegion(false).contains(
-                                (int) event.getRawX(), (int) event.getRawY())) {
-                            mCallbacks.onTouchOutside();
-                        }
-                    }
-                });
-    }
-
-    void stopInputListening() {
-        if (mInputMonitor != null) {
-            mInputMonitor.dispose();
-            mInputMonitor = null;
-        }
-        if (mInputEventReceiver != null) {
-            mInputEventReceiver.dispose();
-            mInputEventReceiver = null;
-        }
-    }
-
-    @Override // View
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
-        mScreenshotStatic = requireNonNull(findViewById(R.id.screenshot_static));
-        mScreenshotPreview = requireNonNull(findViewById(R.id.screenshot_preview));
-
-        mScreenshotPreviewBorder = requireNonNull(
-                findViewById(R.id.screenshot_preview_border));
-        mScreenshotPreview.setClipToOutline(true);
-        mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge));
-
-        mActionsContainerBackground = requireNonNull(findViewById(
-                R.id.actions_container_background));
-        mActionsContainer = requireNonNull(findViewById(R.id.actions_container));
-        mActionsView = requireNonNull(findViewById(R.id.screenshot_actions));
-        mDismissButton = requireNonNull(findViewById(R.id.screenshot_dismiss_button));
-        mScrollablePreview = requireNonNull(findViewById(R.id.screenshot_scrollable_preview));
-        mScreenshotFlash = requireNonNull(findViewById(R.id.screenshot_flash));
-        mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip));
-        mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
-        mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
-
-        setFocusable(true);
-        mActionsContainer.setScrollX(0);
-
-        mNavMode = getResources().getInteger(
-                com.android.internal.R.integer.config_navBarInteractionMode);
-        mOrientationPortrait =
-                getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
-        mDirectionLTR =
-                getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
-
-        // Get focus so that the key events go to the layout.
-        setFocusableInTouchMode(true);
-        requestFocus();
-
-        mScreenshotStatic.setCallbacks(new DraggableConstraintLayout.SwipeDismissCallbacks() {
-            @Override
-            public void onInteraction() {
-                mCallbacks.onUserInteraction();
-            }
-
-            @Override
-            public void onSwipeDismissInitiated(Animator animator) {
-                if (DEBUG_DISMISS) {
-                    Log.d(ScreenshotView.TAG, "dismiss triggered via swipe gesture");
-                }
-                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0,
-                        mPackageName);
-            }
-
-            @Override
-            public void onDismissComplete() {
-                if (mInteractionJankMonitor.isInstrumenting(CUJ_TAKE_SCREENSHOT)) {
-                    mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
-                }
-                mCallbacks.onDismiss();
-            }
-        });
-    }
-
-    View getScreenshotPreview() {
-        return mScreenshotPreview;
-    }
-
-    void setUiEventLogger(UiEventLogger uiEventLogger) {
-        mUiEventLogger = uiEventLogger;
-    }
-
-    void setCallbacks(ScreenshotViewCallback callbacks) {
-        mCallbacks = callbacks;
-    }
-
-    void setFlags(FeatureFlags flags) {
-        mFlags = flags;
-    }
-
-    void setScreenshot(Bitmap bitmap, Insets screenInsets) {
-        mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
-    }
-
-    void setScreenshot(ScreenshotData screenshot) {
-        mScreenshotData = screenshot;
-        setScreenshot(screenshot.getBitmap(), screenshot.getInsets());
-        mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, screenshot.getBitmap(),
-                screenshot.getInsets()));
-    }
-
-    void setPackageName(String packageName) {
-        mPackageName = packageName;
-    }
-
-    void setDefaultDisplay(int displayId) {
-        mDefaultDisplay = displayId;
-    }
-
-    void updateInsets(WindowInsets insets) {
-        int orientation = mContext.getResources().getConfiguration().orientation;
-        mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
-        FrameLayout.LayoutParams p =
-                (FrameLayout.LayoutParams) mScreenshotStatic.getLayoutParams();
-        DisplayCutout cutout = insets.getDisplayCutout();
-        Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
-        if (cutout == null) {
-            p.setMargins(0, 0, 0, navBarInsets.bottom);
-        } else {
-            Insets waterfall = cutout.getWaterfallInsets();
-            if (mOrientationPortrait) {
-                p.setMargins(
-                        waterfall.left,
-                        Math.max(cutout.getSafeInsetTop(), waterfall.top),
-                        waterfall.right,
-                        Math.max(cutout.getSafeInsetBottom(),
-                                Math.max(navBarInsets.bottom, waterfall.bottom)));
-            } else {
-                p.setMargins(
-                        Math.max(cutout.getSafeInsetLeft(), waterfall.left),
-                        waterfall.top,
-                        Math.max(cutout.getSafeInsetRight(), waterfall.right),
-                        Math.max(navBarInsets.bottom, waterfall.bottom));
-            }
-        }
-        mScreenshotStatic.setLayoutParams(p);
-        mScreenshotStatic.requestLayout();
-    }
-
-    void updateOrientation(WindowInsets insets) {
-        int orientation = mContext.getResources().getConfiguration().orientation;
-        mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
-        updateInsets(insets);
-        ViewGroup.LayoutParams params = mScreenshotPreview.getLayoutParams();
-        if (mOrientationPortrait) {
-            params.width = (int) mFixedSize;
-            params.height = LayoutParams.WRAP_CONTENT;
-            mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_START);
-        } else {
-            params.width = LayoutParams.WRAP_CONTENT;
-            params.height = (int) mFixedSize;
-            mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_END);
-        }
-
-        mScreenshotPreview.setLayoutParams(params);
-    }
-
-    AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) {
-        if (DEBUG_ANIM) {
-            Log.d(TAG, "createAnim: bounds=" + bounds + " showFlash=" + showFlash);
-        }
-
-        Rect targetPosition = new Rect();
-        mScreenshotPreview.getHitRect(targetPosition);
-
-        // ratio of preview width, end vs. start size
-        float cornerScale =
-                mFixedSize / (mOrientationPortrait ? bounds.width() : bounds.height());
-        final float currentScale = 1 / cornerScale;
-
-        AnimatorSet dropInAnimation = new AnimatorSet();
-        ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1);
-        flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS);
-        flashInAnimator.setInterpolator(mFastOutSlowIn);
-        flashInAnimator.addUpdateListener(animation ->
-                mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
-
-        ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0);
-        flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS);
-        flashOutAnimator.setInterpolator(mFastOutSlowIn);
-        flashOutAnimator.addUpdateListener(animation ->
-                mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
-
-        // animate from the current location, to the static preview location
-        final PointF startPos = new PointF(bounds.centerX(), bounds.centerY());
-        final PointF finalPos = new PointF(targetPosition.exactCenterX(),
-                targetPosition.exactCenterY());
-
-        // Shift to screen coordinates so that the animation runs on top of the entire screen,
-        // including e.g. bars covering the display cutout.
-        int[] locInScreen = mScreenshotPreview.getLocationOnScreen();
-        startPos.offset(targetPosition.left - locInScreen[0], targetPosition.top - locInScreen[1]);
-
-        if (DEBUG_ANIM) {
-            Log.d(TAG, "toCorner: startPos=" + startPos);
-            Log.d(TAG, "toCorner: finalPos=" + finalPos);
-        }
-
-        ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1);
-        toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS);
-
-        toCorner.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mScreenshotPreview.setScaleX(currentScale);
-                mScreenshotPreview.setScaleY(currentScale);
-                mScreenshotPreview.setVisibility(View.VISIBLE);
-                if (mAccessibilityManager.isEnabled()) {
-                    mDismissButton.setAlpha(0);
-                    mDismissButton.setVisibility(View.VISIBLE);
-                }
-            }
-        });
-
-        float xPositionPct =
-                SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
-        float dismissPct =
-                SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
-        float scalePct =
-                SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
-        toCorner.addUpdateListener(animation -> {
-            float t = animation.getAnimatedFraction();
-            if (t < scalePct) {
-                float scale = MathUtils.lerp(
-                        currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct));
-                mScreenshotPreview.setScaleX(scale);
-                mScreenshotPreview.setScaleY(scale);
-            } else {
-                mScreenshotPreview.setScaleX(1);
-                mScreenshotPreview.setScaleY(1);
-            }
-
-            if (t < xPositionPct) {
-                float xCenter = MathUtils.lerp(startPos.x, finalPos.x,
-                        mFastOutSlowIn.getInterpolation(t / xPositionPct));
-                mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f);
-            } else {
-                mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
-            }
-            float yCenter = MathUtils.lerp(
-                    startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t));
-            mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f);
-
-            if (t >= dismissPct) {
-                mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct));
-                float currentX = mScreenshotPreview.getX();
-                float currentY = mScreenshotPreview.getY();
-                mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f);
-                if (mDirectionLTR) {
-                    mDismissButton.setX(currentX + mScreenshotPreview.getWidth()
-                            - mDismissButton.getWidth() / 2f);
-                } else {
-                    mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f);
-                }
-            }
-        });
-
-        mScreenshotFlash.setAlpha(0f);
-        mScreenshotFlash.setVisibility(View.VISIBLE);
-
-        ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
-        borderFadeIn.setDuration(100);
-        borderFadeIn.addUpdateListener((animation) -> {
-            float borderAlpha = animation.getAnimatedFraction();
-            mScreenshotPreviewBorder.setAlpha(borderAlpha);
-            mScreenshotBadge.setAlpha(borderAlpha);
-        });
-
-        if (showFlash) {
-            dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
-            dropInAnimation.play(flashOutAnimator).with(toCorner);
-        } else {
-            dropInAnimation.play(toCorner);
-        }
-        dropInAnimation.play(borderFadeIn).after(toCorner);
-
-        dropInAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT);
-            }
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                InteractionJankMonitor.Configuration.Builder builder =
-                        InteractionJankMonitor.Configuration.Builder.withView(
-                                        CUJ_TAKE_SCREENSHOT, mScreenshotPreview)
-                                .setTag("DropIn");
-                mInteractionJankMonitor.begin(builder);
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (DEBUG_ANIM) {
-                    Log.d(TAG, "drop-in animation ended");
-                }
-                mDismissButton.setOnClickListener(view -> {
-                    if (DEBUG_INPUT) {
-                        Log.d(TAG, "dismiss button clicked");
-                    }
-                    mUiEventLogger.log(
-                            ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, 0, mPackageName);
-                    animateDismissal();
-                });
-                mDismissButton.setAlpha(1);
-                float dismissOffset = mDismissButton.getWidth() / 2f;
-                float finalDismissX = mDirectionLTR
-                        ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f
-                        : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f;
-                mDismissButton.setX(finalDismissX);
-                mDismissButton.setY(
-                        finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f);
-                mScreenshotPreview.setScaleX(1);
-                mScreenshotPreview.setScaleY(1);
-                mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
-                mScreenshotPreview.setY(finalPos.y - mScreenshotPreview.getHeight() / 2f);
-                requestLayout();
-                mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
-                createScreenshotActionsShadeAnimation().start();
-            }
-        });
-
-        return dropInAnimation;
-    }
-
-    ValueAnimator createScreenshotActionsShadeAnimation() {
-        // By default the activities won't be able to start immediately; override this to keep
-        // the same behavior as if started from a notification
-        try {
-            ActivityManager.getService().resumeAppSwitches();
-        } catch (RemoteException e) {
-        }
-
-        ArrayList<OverlayActionChip> chips = new ArrayList<>();
-
-        mShareChip.setContentDescription(mContext.getString(R.string.screenshot_share_description));
-        mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
-        mShareChip.setOnClickListener(v -> {
-            mShareChip.setIsPending(true);
-            mEditChip.setIsPending(false);
-            if (mQuickShareChip != null) {
-                mQuickShareChip.setIsPending(false);
-            }
-            mPendingInteraction = PendingInteraction.SHARE;
-        });
-        chips.add(mShareChip);
-
-        mEditChip.setContentDescription(
-                mContext.getString(R.string.screenshot_edit_description));
-        mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit),
-                true);
-        mEditChip.setOnClickListener(v -> {
-            mEditChip.setIsPending(true);
-            mShareChip.setIsPending(false);
-            if (mQuickShareChip != null) {
-                mQuickShareChip.setIsPending(false);
-            }
-            mPendingInteraction = PendingInteraction.EDIT;
-        });
-        chips.add(mEditChip);
-
-        mScreenshotPreview.setOnClickListener(v -> {
-            mShareChip.setIsPending(false);
-            mEditChip.setIsPending(false);
-            if (mQuickShareChip != null) {
-                mQuickShareChip.setIsPending(false);
-            }
-            mPendingInteraction = PendingInteraction.PREVIEW;
-        });
-
-        mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label));
-        mScrollChip.setIcon(Icon.createWithResource(mContext,
-                R.drawable.ic_screenshot_scroll), true);
-        chips.add(mScrollChip);
-
-        // remove the margin from the last chip so that it's correctly aligned with the end
-        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
-                mActionsView.getChildAt(0).getLayoutParams();
-        params.setMarginEnd(0);
-        mActionsView.getChildAt(0).setLayoutParams(params);
-
-        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
-        animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS);
-        float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS
-                / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS;
-        mActionsContainer.setAlpha(0f);
-        mActionsContainerBackground.setAlpha(0f);
-        mActionsContainer.setVisibility(View.VISIBLE);
-        mActionsContainerBackground.setVisibility(View.VISIBLE);
-
-        animator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT);
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
-            }
-
-            @Override
-            public void onAnimationStart(Animator animation) {
-                InteractionJankMonitor.Configuration.Builder builder =
-                        InteractionJankMonitor.Configuration.Builder.withView(
-                                        CUJ_TAKE_SCREENSHOT, mScreenshotStatic)
-                                .setTag("Actions")
-                                .setTimeout(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
-                mInteractionJankMonitor.begin(builder);
-            }
-        });
-
-        animator.addUpdateListener(animation -> {
-            float t = animation.getAnimatedFraction();
-            float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
-            mActionsContainer.setAlpha(containerAlpha);
-            mActionsContainerBackground.setAlpha(containerAlpha);
-            float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X
-                    + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X));
-            mActionsContainer.setScaleX(containerScale);
-            mActionsContainerBackground.setScaleX(containerScale);
-            for (OverlayActionChip chip : chips) {
-                chip.setAlpha(t);
-                chip.setScaleX(1 / containerScale); // invert to keep size of children constant
-            }
-            mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
-            mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
-            mActionsContainerBackground.setPivotX(
-                    mDirectionLTR ? 0 : mActionsContainerBackground.getWidth());
-        });
-        return animator;
-    }
-
-    void badgeScreenshot(@Nullable Drawable badge) {
-        mScreenshotBadge.setImageDrawable(badge);
-        mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
-    }
-
-    void setChipIntents(ScreenshotController.SavedImageData imageData) {
-        mShareChip.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
-            prepareSharedTransition();
-
-            Intent shareIntent = ActionIntentCreator.INSTANCE.createShareWithSubject(
-                    imageData.uri, imageData.subject);
-            mCallbacks.onAction(shareIntent, imageData.owner, false);
-
-        });
-        mEditChip.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
-            prepareSharedTransition();
-            mCallbacks.onAction(
-                    ActionIntentCreator.INSTANCE.createEdit(imageData.uri, mContext),
-                    imageData.owner, true);
-        });
-        mScreenshotPreview.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
-            prepareSharedTransition();
-            mCallbacks.onAction(
-                    ActionIntentCreator.INSTANCE.createEdit(imageData.uri, mContext),
-                    imageData.owner, true);
-        });
-        if (mQuickShareChip != null) {
-            if (imageData.quickShareAction != null) {
-                mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
-                        () -> {
-                            mUiEventLogger.log(
-                                    ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0,
-                                    mPackageName);
-                            animateDismissal();
-                        });
-            } else {
-                // hide chip and unset pending interaction if necessary, since we don't actually
-                // have a useable quick share intent
-                Log.wtf(TAG, "Showed quick share chip, but quick share intent was null");
-                if (mPendingInteraction == PendingInteraction.QUICK_SHARE) {
-                    mPendingInteraction = null;
-                }
-                mQuickShareChip.setVisibility(GONE);
-            }
-        }
-
-        if (mPendingInteraction != null) {
-            switch (mPendingInteraction) {
-                case PREVIEW:
-                    mScreenshotPreview.callOnClick();
-                    break;
-                case SHARE:
-                    mShareChip.callOnClick();
-                    break;
-                case EDIT:
-                    mEditChip.callOnClick();
-                    break;
-                case QUICK_SHARE:
-                    mQuickShareChip.callOnClick();
-                    break;
-            }
-        } else {
-            LayoutInflater inflater = LayoutInflater.from(mContext);
-
-            for (Notification.Action smartAction : imageData.smartActions) {
-                OverlayActionChip actionChip = (OverlayActionChip) inflater.inflate(
-                        R.layout.overlay_action_chip, mActionsView, false);
-                actionChip.setText(smartAction.title);
-                actionChip.setIcon(smartAction.getIcon(), false);
-                actionChip.setPendingIntent(smartAction.actionIntent,
-                        () -> {
-                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED,
-                                    0, mPackageName);
-                            animateDismissal();
-                        });
-                actionChip.setAlpha(1);
-                mActionsView.addView(actionChip, mActionsView.getChildCount() - 1);
-                mSmartChips.add(actionChip);
-            }
-        }
-    }
-
-    void addQuickShareChip(Notification.Action quickShareAction) {
-        if (mQuickShareChip != null) {
-            mSmartChips.remove(mQuickShareChip);
-            mActionsView.removeView(mQuickShareChip);
-        }
-        if (mPendingInteraction == PendingInteraction.QUICK_SHARE) {
-            mPendingInteraction = null;
-        }
-        if (mPendingInteraction == null) {
-            LayoutInflater inflater = LayoutInflater.from(mContext);
-            mQuickShareChip = (OverlayActionChip) inflater.inflate(
-                    R.layout.overlay_action_chip, mActionsView, false);
-            mQuickShareChip.setText(quickShareAction.title);
-            mQuickShareChip.setIcon(quickShareAction.getIcon(), false);
-            mQuickShareChip.setOnClickListener(v -> {
-                mShareChip.setIsPending(false);
-                mEditChip.setIsPending(false);
-                mQuickShareChip.setIsPending(true);
-                mPendingInteraction = PendingInteraction.QUICK_SHARE;
-            });
-            mQuickShareChip.setAlpha(1);
-            mActionsView.addView(mQuickShareChip);
-            mSmartChips.add(mQuickShareChip);
-        }
-    }
-
-    private Rect scrollableAreaOnScreen(ScrollCaptureResponse response) {
-        Rect r = new Rect(response.getBoundsInWindow());
-        Rect windowInScreen = response.getWindowBounds();
-        r.offset(windowInScreen.left, windowInScreen.top);
-        r.intersect(new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
-        return r;
-    }
-
-    void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd,
-            ScrollCaptureController.LongScreenshot longScreenshot) {
-        mPendingSharedTransition = true;
-        AnimatorSet animSet = new AnimatorSet();
-
-        ValueAnimator scrimAnim = ValueAnimator.ofFloat(0, 1);
-        scrimAnim.addUpdateListener(animation ->
-                mScrollingScrim.setAlpha(1 - animation.getAnimatedFraction()));
-
-        if (mShowScrollablePreview) {
-            mScrollablePreview.setImageBitmap(longScreenshot.toBitmap());
-            float startX = mScrollablePreview.getX();
-            float startY = mScrollablePreview.getY();
-            int[] locInScreen = mScrollablePreview.getLocationOnScreen();
-            destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]);
-            mScrollablePreview.setPivotX(0);
-            mScrollablePreview.setPivotY(0);
-            mScrollablePreview.setAlpha(1f);
-            float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth();
-            Matrix matrix = new Matrix();
-            matrix.setScale(currentScale, currentScale);
-            matrix.postTranslate(
-                    longScreenshot.getLeft() * currentScale,
-                    longScreenshot.getTop() * currentScale);
-            mScrollablePreview.setImageMatrix(matrix);
-            float destinationScale = destination.width() / (float) mScrollablePreview.getWidth();
-
-            ValueAnimator previewAnim = ValueAnimator.ofFloat(0, 1);
-            previewAnim.addUpdateListener(animation -> {
-                float t = animation.getAnimatedFraction();
-                float currScale = MathUtils.lerp(1, destinationScale, t);
-                mScrollablePreview.setScaleX(currScale);
-                mScrollablePreview.setScaleY(currScale);
-                mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t));
-                mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t));
-            });
-            ValueAnimator previewFadeAnim = ValueAnimator.ofFloat(1, 0);
-            previewFadeAnim.addUpdateListener(animation ->
-                    mScrollablePreview.setAlpha(1 - animation.getAnimatedFraction()));
-            animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim);
-            previewAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    onTransitionEnd.run();
-                }
-            });
-        } else {
-            // if we switched orientations between the original screenshot and the long screenshot
-            // capture, just fade out the scrim instead of running the preview animation
-            animSet.play(scrimAnim);
-            animSet.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    onTransitionEnd.run();
-                }
-            });
-        }
-        animSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                super.onAnimationEnd(animation);
-                mCallbacks.onDismiss();
-            }
-        });
-        animSet.start();
-    }
-
-    void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap,
-            Bitmap newBitmap, boolean screenshotTakenInPortrait) {
-        mShowScrollablePreview = (screenshotTakenInPortrait == mOrientationPortrait);
-
-        mScrollingScrim.setImageBitmap(newBitmap);
-        mScrollingScrim.setVisibility(View.VISIBLE);
-
-        if (mShowScrollablePreview) {
-            Rect scrollableArea = scrollableAreaOnScreen(response);
-
-            float scale = mFixedSize
-                    / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight());
-            ConstraintLayout.LayoutParams params =
-                    (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams();
-
-            params.width = (int) (scale * scrollableArea.width());
-            params.height = (int) (scale * scrollableArea.height());
-            Matrix matrix = new Matrix();
-            matrix.setScale(scale, scale);
-            matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale);
-
-            mScrollablePreview.setTranslationX(scale
-                    * (mDirectionLTR ? scrollableArea.left : scrollableArea.right - getWidth()));
-            mScrollablePreview.setTranslationY(scale * scrollableArea.top);
-            mScrollablePreview.setImageMatrix(matrix);
-            mScrollablePreview.setImageBitmap(screenBitmap);
-            mScrollablePreview.setVisibility(View.VISIBLE);
-        }
-        mDismissButton.setVisibility(View.GONE);
-        mActionsContainer.setVisibility(View.GONE);
-        // set these invisible, but not gone, so that the views are laid out correctly
-        mActionsContainerBackground.setVisibility(View.INVISIBLE);
-        mScreenshotPreviewBorder.setVisibility(View.INVISIBLE);
-        mScreenshotPreview.setVisibility(View.INVISIBLE);
-        mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP);
-        ValueAnimator anim = ValueAnimator.ofFloat(0, .3f);
-        anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList(
-                ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0))));
-        anim.setDuration(200);
-        anim.start();
-    }
-
-    void restoreNonScrollingUi() {
-        mScrollChip.setVisibility(View.GONE);
-        mScrollablePreview.setVisibility(View.GONE);
-        mScrollingScrim.setVisibility(View.GONE);
-
-        if (mAccessibilityManager.isEnabled()) {
-            mDismissButton.setVisibility(View.VISIBLE);
-        }
-        mActionsContainer.setVisibility(View.VISIBLE);
-        mActionsContainerBackground.setVisibility(View.VISIBLE);
-        mScreenshotPreviewBorder.setVisibility(View.VISIBLE);
-        mScreenshotPreview.setVisibility(View.VISIBLE);
-        // reset the timeout
-        mCallbacks.onUserInteraction();
-    }
-
-    boolean isDismissing() {
-        return mScreenshotStatic.isDismissing();
-    }
-
-    boolean isPendingSharedTransition() {
-        return mPendingSharedTransition;
-    }
-
-    void animateDismissal() {
-        mScreenshotStatic.dismiss();
-    }
-
-    void reset() {
-        if (DEBUG_UI) {
-            Log.d(TAG, "reset screenshot view");
-        }
-        mScreenshotStatic.cancelDismissal();
-        if (DEBUG_WINDOW) {
-            Log.d(TAG, "removing OnComputeInternalInsetsListener");
-        }
-        // Make sure we clean up the view tree observer
-        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
-        // Clear any references to the bitmap
-        mScreenshotPreview.setImageDrawable(null);
-        mScreenshotPreview.setVisibility(View.INVISIBLE);
-        mScreenshotPreview.setAlpha(1f);
-        mScreenshotPreviewBorder.setAlpha(0);
-        mScreenshotBadge.setAlpha(0f);
-        mScreenshotBadge.setVisibility(View.GONE);
-        mScreenshotBadge.setImageDrawable(null);
-        mPendingSharedTransition = false;
-        mActionsContainerBackground.setVisibility(View.INVISIBLE);
-        mActionsContainer.setVisibility(View.GONE);
-        mDismissButton.setVisibility(View.GONE);
-        mScrollingScrim.setVisibility(View.GONE);
-        mScrollablePreview.setVisibility(View.GONE);
-        mScreenshotStatic.setTranslationX(0);
-        mScreenshotPreview.setContentDescription(
-                mContext.getResources().getString(R.string.screenshot_preview_description));
-        mScreenshotPreview.setOnClickListener(null);
-        mShareChip.setOnClickListener(null);
-        mScrollingScrim.setVisibility(View.GONE);
-        mEditChip.setOnClickListener(null);
-        mShareChip.setIsPending(false);
-        mEditChip.setIsPending(false);
-        mPendingInteraction = null;
-        for (OverlayActionChip chip : mSmartChips) {
-            mActionsView.removeView(chip);
-        }
-        mSmartChips.clear();
-        mQuickShareChip = null;
-        setAlpha(1);
-        mScreenshotStatic.setAlpha(1);
-        mScreenshotData = null;
-    }
-
-    private void prepareSharedTransition() {
-        mPendingSharedTransition = true;
-        // fade out non-preview UI
-        createScreenshotFadeDismissAnimation().start();
-    }
-
-    ValueAnimator createScreenshotFadeDismissAnimation() {
-        ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
-        alphaAnim.addUpdateListener(animation -> {
-            float alpha = 1 - animation.getAnimatedFraction();
-            mDismissButton.setAlpha(alpha);
-            mActionsContainerBackground.setAlpha(alpha);
-            mActionsContainer.setAlpha(alpha);
-            mScreenshotPreviewBorder.setAlpha(alpha);
-            mScreenshotBadge.setAlpha(alpha);
-        });
-        alphaAnim.setDuration(600);
-        return alphaAnim;
-    }
-
-    /**
-     * Create a drawable using the size of the bitmap and insets as the fractional inset parameters.
-     */
-    private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) {
-        int insettedWidth = bitmap.getWidth() - insets.left - insets.right;
-        int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom;
-
-        BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
-        if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
-                || bitmap.getHeight() == 0) {
-            Log.e(TAG, "Can't create inset drawable, using 0 insets bitmap and insets create "
-                    + "degenerate region: " + bitmap.getWidth() + "x" + bitmap.getHeight() + " "
-                    + bitmapDrawable);
-            return bitmapDrawable;
-        }
-
-        InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable,
-                -1f * insets.left / insettedWidth,
-                -1f * insets.top / insettedHeight,
-                -1f * insets.right / insettedWidth,
-                -1f * insets.bottom / insettedHeight);
-
-        if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
-            // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need
-            // to fill in the background of the drawable.
-            return new LayerDrawable(new Drawable[]{
-                    new ColorDrawable(Color.BLACK), insetDrawable});
-        } else {
-            return insetDrawable;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
deleted file mode 100644
index df93a5e..0000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.screenshot
-
-import android.animation.Animator
-import android.app.Notification
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Rect
-import android.view.ScrollCaptureResponse
-import android.view.View
-import android.view.ViewGroup
-import android.view.WindowInsets
-import com.android.systemui.screenshot.scroll.ScrollCaptureController
-
-/** Abstraction of the surface between ScreenshotController and ScreenshotView */
-interface ScreenshotViewProxy {
-    val view: ViewGroup
-    val screenshotPreview: View
-
-    var packageName: String
-    var callbacks: ScreenshotView.ScreenshotViewCallback?
-    var screenshot: ScreenshotData?
-
-    val isAttachedToWindow: Boolean
-    val isDismissing: Boolean
-    val isPendingSharedTransition: Boolean
-
-    fun reset()
-    fun updateInsets(insets: WindowInsets)
-    fun updateOrientation(insets: WindowInsets)
-    fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator
-    fun addQuickShareChip(quickShareAction: Notification.Action)
-    fun setChipIntents(imageData: ScreenshotController.SavedImageData)
-    fun requestDismissal(event: ScreenshotEvent?)
-
-    fun showScrollChip(packageName: String, onClick: Runnable)
-    fun hideScrollChip()
-    fun prepareScrollingTransition(
-        response: ScrollCaptureResponse,
-        screenBitmap: Bitmap,
-        newScreenshot: Bitmap,
-        screenshotTakenInPortrait: Boolean,
-        onTransitionPrepared: Runnable,
-    )
-    fun startLongScreenshotTransition(
-        transitionDestination: Rect,
-        onTransitionEnd: Runnable,
-        longScreenshot: ScrollCaptureController.LongScreenshot
-    )
-    fun restoreNonScrollingUi()
-    fun fadeForSharedTransition()
-
-    fun stopInputListening()
-    fun requestFocus()
-    fun announceForAccessibility(string: String)
-    fun prepareEntranceAnimation(runnable: Runnable)
-
-    interface Factory {
-        fun getProxy(context: Context, displayId: Int): ScreenshotViewProxy
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 2699657..07f6e85 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -219,7 +219,7 @@
     }
 
     private fun getScreenshotController(display: Display): ScreenshotController {
-        val controller = screenshotController ?: screenshotControllerFactory.create(display, false)
+        val controller = screenshotController ?: screenshotControllerFactory.create(display)
         screenshotController = controller
         return controller
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 56ba1af4..682f848 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -24,12 +24,10 @@
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
-import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
 import com.android.systemui.screenshot.ScreenshotSoundController;
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
 import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
-import com.android.systemui.screenshot.ScreenshotViewProxy;
 import com.android.systemui.screenshot.TakeScreenshotExecutor;
 import com.android.systemui.screenshot.TakeScreenshotExecutorImpl;
 import com.android.systemui.screenshot.TakeScreenshotService;
@@ -92,8 +90,4 @@
             AccessibilityManager accessibilityManager) {
         return new ScreenshotViewModel(accessibilityManager);
     }
-
-    @Binds
-    abstract ScreenshotViewProxy.Factory bindScreenshotViewProxyFactory(
-            ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 04de2c2..c1caeed 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1774,8 +1774,9 @@
         // the small clock here
         // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks
         if (!MigrateClocksToBlueprint.isEnabled()) {
+            boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled();
             if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf()
-                    && hasVisibleNotifications() && isOnAod()) {
+                    && hasVisibleNotifications() && (isOnAod() || bypassEnabled)) {
                 return SMALL;
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index c1f8a0b..45f359e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -55,7 +55,11 @@
     /** Whether the shade can be expanded from QQS to QS. */
     val isExpandToQsEnabled: Flow<Boolean>
 
-    /** The version of the shade layout to use. */
+    /**
+     * The version of the shade layout to use.
+     *
+     * Note: Most likely, you want to read [isShadeLayoutWide] instead of this.
+     */
     val shadeMode: StateFlow<ShadeMode>
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
index 8214a24..a8199a4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
@@ -29,6 +29,8 @@
     /**
      * The split shade where, on large screens and unfolded foldables, the QS and notification parts
      * are placed side-by-side and expand/collapse as a single panel.
+     *
+     * Note: This isn't the only mode where the shade is wide.
      */
     data object Split : ShadeMode
 
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
index 2f58b35..ea4e065 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt
@@ -18,51 +18,35 @@
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.smartspace.SmartspacePrecondition
 import com.android.systemui.smartspace.SmartspaceTargetFilter
-import com.android.systemui.smartspace.data.repository.SmartspaceRepositoryModule
 import com.android.systemui.smartspace.preconditions.LockscreenPrecondition
 import dagger.Binds
 import dagger.BindsOptionalOf
 import dagger.Module
 import javax.inject.Named
 
-@Module(subcomponents = [SmartspaceViewComponent::class],
-    includes = [SmartspaceRepositoryModule::class])
+@Module(subcomponents = [SmartspaceViewComponent::class])
 abstract class SmartspaceModule {
     @Module
     companion object {
-        /**
-         * The BcSmartspaceDataProvider for dreams.
-         */
+        /** The BcSmartspaceDataProvider for dreams. */
         const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin"
 
-        /**
-         * The BcSmartspaceDataPlugin for the standalone weather on dream.
-         */
+        /** The BcSmartspaceDataPlugin for the standalone weather on dream. */
         const val DREAM_WEATHER_SMARTSPACE_DATA_PLUGIN = "dream_weather_smartspace_data_plugin"
 
-        /**
-         * The target filter for smartspace over lockscreen.
-         */
+        /** The target filter for smartspace over lockscreen. */
         const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter"
 
-        /**
-         * The precondition for smartspace over lockscreen
-         */
+        /** The precondition for smartspace over lockscreen */
         const val LOCKSCREEN_SMARTSPACE_PRECONDITION = "lockscreen_smartspace_precondition"
 
-        /**
-         * The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd).
-         */
+        /** The BcSmartspaceDataPlugin for the standalone date (+alarm+dnd). */
         const val DATE_SMARTSPACE_DATA_PLUGIN = "date_smartspace_data_plugin"
 
-        /**
-         * The BcSmartspaceDataPlugin for the standalone weather.
-         */
+        /** The BcSmartspaceDataPlugin for the standalone weather. */
         const val WEATHER_SMARTSPACE_DATA_PLUGIN = "weather_smartspace_data_plugin"
 
-        /**
-         * The BcSmartspaceDataProvider for the glanceable hub.
-         */
+        /** The BcSmartspaceDataProvider for the glanceable hub. */
         const val GLANCEABLE_HUB_SMARTSPACE_DATA_PLUGIN = "glanceable_hub_smartspace_data_plugin"
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt
deleted file mode 100644
index 52a1c15..0000000
--- a/packages/SystemUI/src/com/android/systemui/smartspace/data/repository/SmartspaceRepository.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2023 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.smartspace.data.repository
-
-import android.app.smartspace.SmartspaceTarget
-import android.os.Parcelable
-import android.widget.RemoteViews
-import com.android.systemui.communal.smartspace.CommunalSmartspaceController
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import java.util.concurrent.Executor
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onStart
-
-interface SmartspaceRepository {
-    /** Whether [RemoteViews] are passed through smartspace targets. */
-    val isSmartspaceRemoteViewsEnabled: Boolean
-
-    /** Smartspace targets for the communal surface. */
-    val communalSmartspaceTargets: Flow<List<SmartspaceTarget>>
-}
-
-@SysUISingleton
-class SmartspaceRepositoryImpl
-@Inject
-constructor(
-    private val communalSmartspaceController: CommunalSmartspaceController,
-    @Main private val uiExecutor: Executor,
-) : SmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener {
-
-    override val isSmartspaceRemoteViewsEnabled: Boolean
-        get() = android.app.smartspace.flags.Flags.remoteViews()
-
-    private val _communalSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> =
-        MutableStateFlow(emptyList())
-    override val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> =
-        _communalSmartspaceTargets
-            .onStart {
-                uiExecutor.execute {
-                    communalSmartspaceController.addListener(
-                        listener = this@SmartspaceRepositoryImpl
-                    )
-                }
-            }
-            .onCompletion {
-                uiExecutor.execute {
-                    communalSmartspaceController.removeListener(
-                        listener = this@SmartspaceRepositoryImpl
-                    )
-                }
-            }
-
-    override fun onSmartspaceTargetsUpdated(targetsNullable: MutableList<out Parcelable>?) {
-        targetsNullable?.let { targets ->
-            _communalSmartspaceTargets.value = targets.filterIsInstance<SmartspaceTarget>()
-        }
-            ?: run { _communalSmartspaceTargets.value = emptyList() }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 5bb2936..c997ac5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -19,6 +19,7 @@
 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
 
+import static com.android.systemui.Flags.fetchBookmarksXmlKeyboardShortcuts;
 import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
 
 import android.annotation.NonNull;
@@ -149,7 +150,7 @@
     private KeyCharacterMap mBackupKeyCharacterMap;
 
     @VisibleForTesting
-    KeyboardShortcutListSearch(Context context, WindowManager windowManager) {
+    KeyboardShortcutListSearch(Context context, WindowManager windowManager, int deviceId) {
         this.mContext = new ContextThemeWrapper(
                 context, R.style.KeyboardShortcutHelper);
         this.mPackageManager = AppGlobals.getPackageManager();
@@ -159,12 +160,12 @@
             this.mWindowManager = mContext.getSystemService(WindowManager.class);
         }
         loadResources(this.mContext);
-        createHardcodedShortcuts();
+        createHardcodedShortcuts(deviceId);
     }
 
-    private static KeyboardShortcutListSearch getInstance(Context context) {
+    private static KeyboardShortcutListSearch getInstance(Context context, int deviceId) {
         if (sInstance == null) {
-            sInstance = new KeyboardShortcutListSearch(context, null);
+            sInstance = new KeyboardShortcutListSearch(context, null, deviceId);
         }
         return sInstance;
     }
@@ -176,7 +177,7 @@
             if (sInstance != null && !sInstance.mContext.equals(context)) {
                 dismiss();
             }
-            getInstance(context).showKeyboardShortcuts(deviceId);
+            getInstance(context, deviceId).showKeyboardShortcuts(deviceId);
         }
     }
 
@@ -367,7 +368,7 @@
                 KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
     }
 
-    private void createHardcodedShortcuts() {
+    private void createHardcodedShortcuts(int deviceId) {
         // Add system shortcuts
         mKeySearchResultMap.put(SHORTCUT_SYSTEM_INDEX, true);
         mSystemGroup.add(getMultiMappingSystemShortcuts(mContext));
@@ -377,7 +378,7 @@
         mInputGroup.add(getMultiMappingInputShortcuts(mContext));
         // Add open apps shortcuts
         final List<KeyboardShortcutMultiMappingGroup> appShortcuts =
-                Arrays.asList(getDefaultMultiMappingApplicationShortcuts());
+                Arrays.asList(getDefaultMultiMappingApplicationShortcuts(deviceId));
         if (appShortcuts != null && !appShortcuts.isEmpty()) {
             mOpenAppsGroup = appShortcuts;
             mKeySearchResultMap.put(SHORTCUT_OPENAPPS_INDEX, true);
@@ -739,35 +740,50 @@
                 shortcutMultiMappingInfoList);
     }
 
-    private KeyboardShortcutMultiMappingGroup getDefaultMultiMappingApplicationShortcuts() {
-        final int userId = mContext.getUserId();
-        PackageInfo assistPackageInfo = getAssistPackageInfo(mContext, mPackageManager, userId);
-        CharSequence categoryTitle =
-                mContext.getString(R.string.keyboard_shortcut_group_applications);
+    private KeyboardShortcutMultiMappingGroup getDefaultMultiMappingApplicationShortcuts(
+            int deviceId) {
         List<ShortcutMultiMappingInfo> shortcutMultiMappingInfos = new ArrayList<>();
+        CharSequence categoryTitle;
+        if (fetchBookmarksXmlKeyboardShortcuts()) {
+            KeyboardShortcutGroup apps =
+                    mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+            List<KeyboardShortcutMultiMappingGroup> shortcuts =
+                    reMapToKeyboardShortcutMultiMappingGroup(Arrays.asList(apps));
+            for (KeyboardShortcutMultiMappingGroup group : shortcuts) {
+                for (ShortcutMultiMappingInfo keyboardShortcutInfo : group.getItems()) {
+                    shortcutMultiMappingInfos.add(keyboardShortcutInfo);
+                }
+            }
+            categoryTitle = apps.getLabel();
+        } else {
+            // Show shortcuts based on AOSP bookmarks.xml
+            categoryTitle = mContext.getString(R.string.keyboard_shortcut_group_applications);
+            final int userId = mContext.getUserId();
+            PackageInfo assistPackageInfo =
+                    getAssistPackageInfo(mContext, mPackageManager, userId);
 
-        String[] intentCategories = {
-                Intent.CATEGORY_APP_BROWSER,
-                Intent.CATEGORY_APP_CONTACTS,
-                Intent.CATEGORY_APP_EMAIL,
-                Intent.CATEGORY_APP_CALENDAR,
-                Intent.CATEGORY_APP_MAPS,
-                Intent.CATEGORY_APP_MUSIC,
-                Intent.CATEGORY_APP_MESSAGING,
-                Intent.CATEGORY_APP_CALCULATOR,
+            String[] intentCategories = {
+                    Intent.CATEGORY_APP_BROWSER,
+                    Intent.CATEGORY_APP_CONTACTS,
+                    Intent.CATEGORY_APP_EMAIL,
+                    Intent.CATEGORY_APP_CALENDAR,
+                    Intent.CATEGORY_APP_MAPS,
+                    Intent.CATEGORY_APP_MUSIC,
+                    Intent.CATEGORY_APP_MESSAGING,
+                    Intent.CATEGORY_APP_CALCULATOR,
+            };
+            String[] shortcutLabels = {
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                    mContext.getString(R.string.keyboard_shortcut_group_applications_calculator)
+            };
 
-        };
-        String[] shortcutLabels = {
-                mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_email),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_music),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
-                mContext.getString(R.string.keyboard_shortcut_group_applications_calculator)
-        };
-        int[] keyCodes = {
+            int[] keyCodes = {
                 KeyEvent.KEYCODE_B,
                 KeyEvent.KEYCODE_C,
                 KeyEvent.KEYCODE_E,
@@ -776,52 +792,44 @@
                 KeyEvent.KEYCODE_P,
                 KeyEvent.KEYCODE_S,
                 KeyEvent.KEYCODE_U,
-        };
+            };
 
-        // Assist.
-        if (assistPackageInfo != null) {
+            // Assist.
             if (assistPackageInfo != null) {
-                final Icon assistIcon = Icon.createWithResource(
-                        assistPackageInfo.applicationInfo.packageName,
-                        assistPackageInfo.applicationInfo.icon);
-                CharSequence assistLabel =
-                        mContext.getString(R.string.keyboard_shortcut_group_applications_assist);
-                KeyboardShortcutInfo assistShortcutInfo = new KeyboardShortcutInfo(
-                        assistLabel,
-                        assistIcon,
-                        KeyEvent.KEYCODE_A,
-                        KeyEvent.META_META_ON);
-                shortcutMultiMappingInfos.add(
-                        new ShortcutMultiMappingInfo(
-                                assistLabel,
-                                assistIcon,
-                                Arrays.asList(new ShortcutKeyGroup(assistShortcutInfo, null))));
+                if (assistPackageInfo != null) {
+                    final Icon assistIcon = Icon.createWithResource(
+                            assistPackageInfo.applicationInfo.packageName,
+                            assistPackageInfo.applicationInfo.icon);
+                    CharSequence assistLabel = mContext.getString(
+                            R.string.keyboard_shortcut_group_applications_assist);
+                    KeyboardShortcutInfo assistShortcutInfo = new KeyboardShortcutInfo(
+                            assistLabel,
+                            assistIcon,
+                            KeyEvent.KEYCODE_A,
+                            KeyEvent.META_META_ON);
+                    shortcutMultiMappingInfos.add(
+                            new ShortcutMultiMappingInfo(
+                                    assistLabel,
+                                    assistIcon,
+                                    Arrays.asList(new ShortcutKeyGroup(assistShortcutInfo, null))));
+                }
             }
-        }
 
-        // Browser (Chrome as default): Meta + B
-        // Contacts: Meta + C
-        // Email (Gmail as default): Meta + E
-        // Gmail: Meta + G
-        // Calendar: Meta + K
-        // Maps: Meta + M
-        // Music: Meta + P
-        // SMS: Meta + S
-        // Calculator: Meta + U
-        for (int i = 0; i < shortcutLabels.length; i++) {
-            final Icon icon = getIconForIntentCategory(intentCategories[i], userId);
-            if (icon != null) {
-                CharSequence label =
-                        shortcutLabels[i];
-                KeyboardShortcutInfo keyboardShortcutInfo = new KeyboardShortcutInfo(
-                        label,
-                        icon,
-                        keyCodes[i],
-                        KeyEvent.META_META_ON);
-                List<ShortcutKeyGroup> shortcutKeyGroups =
-                        Arrays.asList(new ShortcutKeyGroup(keyboardShortcutInfo, null));
-                shortcutMultiMappingInfos.add(
-                        new ShortcutMultiMappingInfo(label, icon, shortcutKeyGroups));
+            for (int i = 0; i < shortcutLabels.length; i++) {
+                final Icon icon = getIconForIntentCategory(intentCategories[i], userId);
+                if (icon != null) {
+                    CharSequence label =
+                            shortcutLabels[i];
+                    KeyboardShortcutInfo keyboardShortcutInfo = new KeyboardShortcutInfo(
+                            label,
+                            icon,
+                            keyCodes[i],
+                            KeyEvent.META_META_ON);
+                    List<ShortcutKeyGroup> shortcutKeyGroups =
+                            Arrays.asList(new ShortcutKeyGroup(keyboardShortcutInfo, null));
+                    shortcutMultiMappingInfos.add(
+                            new ShortcutMultiMappingInfo(label, icon, shortcutKeyGroups));
+                }
             }
         }
 
@@ -1221,7 +1229,8 @@
         String shortcutKeyString = null;
         Drawable shortcutKeyDrawable = null;
         if (info.getBaseCharacter() > Character.MIN_VALUE) {
-            shortcutKeyString = String.valueOf(info.getBaseCharacter());
+            shortcutKeyString = String.valueOf(info.getBaseCharacter())
+                    .toUpperCase(Locale.getDefault());
         } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index a49ca38..da89eea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -20,6 +20,7 @@
 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
 
+import static com.android.systemui.Flags.fetchBookmarksXmlKeyboardShortcuts;
 import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
 
 import android.annotation.NonNull;
@@ -75,6 +76,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Contains functionality for handling keyboard shortcuts.
@@ -133,6 +135,7 @@
 
     @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null;
     @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null;
+    @Nullable private KeyboardShortcutGroup mDefaultApplicationShortcuts = null;
 
     @VisibleForTesting
     KeyboardShortcuts(Context context, WindowManager windowManager) {
@@ -390,6 +393,7 @@
         mReceivedAppShortcutGroups = null;
         mReceivedImeShortcutGroups = null;
 
+        mDefaultApplicationShortcuts = getDefaultApplicationShortcuts(deviceId);
         mWindowManager.requestAppKeyboardShortcuts(
                 result -> {
                     mBackgroundHandler.post(() -> {
@@ -443,10 +447,8 @@
         mReceivedAppShortcutGroups = null;
         mReceivedImeShortcutGroups = null;
 
-        final KeyboardShortcutGroup defaultAppShortcuts =
-                getDefaultApplicationShortcuts();
-        if (defaultAppShortcuts != null) {
-            shortcutGroups.add(defaultAppShortcuts);
+        if (mDefaultApplicationShortcuts != null) {
+            shortcutGroups.add(mDefaultApplicationShortcuts);
         }
         shortcutGroups.add(getSystemShortcuts());
         showKeyboardShortcutsDialog(shortcutGroups);
@@ -499,7 +501,7 @@
         return systemGroup;
     }
 
-    private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
+    private KeyboardShortcutGroup getDefaultApplicationShortcuts(int deviceId) {
         final int userId = mContext.getUserId();
         List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
 
@@ -529,65 +531,77 @@
             }
         }
 
-        // Browser.
-        final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
-        if (browserIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
-                    browserIcon,
-                    KeyEvent.KEYCODE_B,
-                    KeyEvent.META_META_ON));
-        }
+        CharSequence categoryTitle;
+        if (fetchBookmarksXmlKeyboardShortcuts()) {
+            KeyboardShortcutGroup apps =
+                    mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+            categoryTitle = apps.getLabel();
+            keyboardShortcutInfoAppItems.addAll(apps.getItems());
+        } else {
+            categoryTitle = mContext.getString(R.string.keyboard_shortcut_group_applications);
+            // Browser.
+            final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
+            if (browserIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                        browserIcon,
+                        KeyEvent.KEYCODE_B,
+                        KeyEvent.META_META_ON));
+            }
 
 
-        // Contacts.
-        final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
-        if (contactsIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
-                    contactsIcon,
-                    KeyEvent.KEYCODE_C,
-                    KeyEvent.META_META_ON));
-        }
+            // Contacts.
+            final Icon contactsIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_CONTACTS, userId);
+            if (contactsIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                        contactsIcon,
+                        KeyEvent.KEYCODE_C,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Email.
-        final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
-        if (emailIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
-                    emailIcon,
-                    KeyEvent.KEYCODE_E,
-                    KeyEvent.META_META_ON));
-        }
+            // Email.
+            final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
+            if (emailIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                        emailIcon,
+                        KeyEvent.KEYCODE_E,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Messaging.
-        final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
-        if (messagingIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
-                    messagingIcon,
-                    KeyEvent.KEYCODE_S,
-                    KeyEvent.META_META_ON));
-        }
+            // Messaging.
+            final Icon messagingIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_MESSAGING, userId);
+            if (messagingIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                        messagingIcon,
+                        KeyEvent.KEYCODE_S,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Music.
-        final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
-        if (musicIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
-                    musicIcon,
-                    KeyEvent.KEYCODE_P,
-                    KeyEvent.META_META_ON));
-        }
+            // Music.
+            final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
+            if (musicIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                        musicIcon,
+                        KeyEvent.KEYCODE_P,
+                        KeyEvent.META_META_ON));
+            }
 
-        // Calendar.
-        final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
-        if (calendarIcon != null) {
-            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
-                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
-                    calendarIcon,
-                    KeyEvent.KEYCODE_K,
-                    KeyEvent.META_META_ON));
+            // Calendar.
+            final Icon calendarIcon = getIconForIntentCategory(
+                    Intent.CATEGORY_APP_CALENDAR, userId);
+            if (calendarIcon != null) {
+                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
+                        mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                        calendarIcon,
+                        KeyEvent.KEYCODE_K,
+                        KeyEvent.META_META_ON));
+            }
         }
 
         final int itemsSize = keyboardShortcutInfoAppItems.size();
@@ -598,7 +612,7 @@
         // Sorts by label, case insensitive with nulls and/or empty labels last.
         Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
         return new KeyboardShortcutGroup(
-                mContext.getString(R.string.keyboard_shortcut_group_applications),
+                categoryTitle,
                 keyboardShortcutInfoAppItems,
                 true);
     }
@@ -777,7 +791,8 @@
         String shortcutKeyString = null;
         Drawable shortcutKeyDrawable = null;
         if (info.getBaseCharacter() > Character.MIN_VALUE) {
-            shortcutKeyString = String.valueOf(info.getBaseCharacter());
+            shortcutKeyString = String.valueOf(info.getBaseCharacter())
+                    .toUpperCase(Locale.getDefault());
         } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
             shortcutKeyString = mSpecialCharacterNames.get(info.getKeycode());
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index cac3f25..bafec38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -51,33 +51,34 @@
     }
 
     private fun getMessage(): String {
+        val hostDeviceName = state.projectionState.hostDeviceName
         return if (state.projectionState is MediaProjectionState.Projecting.SingleTask) {
             val appBeingSharedName =
                 endMediaProjectionDialogHelper.getAppName(state.projectionState)
-            if (appBeingSharedName != null && state.deviceName != null) {
+            if (appBeingSharedName != null && hostDeviceName != null) {
                 context.getString(
                     R.string.cast_to_other_device_stop_dialog_message_specific_app_with_device,
                     appBeingSharedName,
-                    state.deviceName,
+                    hostDeviceName,
                 )
             } else if (appBeingSharedName != null) {
                 context.getString(
                     R.string.cast_to_other_device_stop_dialog_message_specific_app,
                     appBeingSharedName,
                 )
-            } else if (state.deviceName != null) {
+            } else if (hostDeviceName != null) {
                 context.getString(
                     R.string.cast_to_other_device_stop_dialog_message_generic_with_device,
-                    state.deviceName
+                    hostDeviceName,
                 )
             } else {
                 context.getString(R.string.cast_to_other_device_stop_dialog_message_generic)
             }
         } else {
-            if (state.deviceName != null) {
+            if (hostDeviceName != null) {
                 context.getString(
                     R.string.cast_to_other_device_stop_dialog_message_entire_screen_with_device,
-                    state.deviceName
+                    hostDeviceName,
                 )
             } else {
                 context.getString(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 4183cdd..f5e17df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import androidx.annotation.DrawableRes
-import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -58,7 +57,6 @@
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val mediaRouterChipInteractor: MediaRouterChipInteractor,
     private val systemClock: SystemClock,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
 ) : OngoingActivityChipViewModel {
     /**
@@ -175,7 +173,6 @@
             startTimeMs = systemClock.elapsedRealtime(),
             createDialogLaunchOnClickListener(
                 createCastScreenToOtherDeviceDialogDelegate(state),
-                dialogTransitionAnimator,
             ),
         )
     }
@@ -191,7 +188,6 @@
             colors = ColorsModel.Red,
             createDialogLaunchOnClickListener(
                 createGenericCastToOtherDeviceDialogDelegate(deviceName),
-                dialogTransitionAnimator,
             ),
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index ce60fab..191c221 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -71,11 +71,11 @@
                             {
                                 str1 = type.name
                                 str2 = state.hostPackage
+                                str3 = state.hostDeviceName
                             },
-                            { "State: Projecting(type=$str1 hostPackage=$str2)" }
+                            { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" }
                         )
-                        // TODO(b/351851835): Get the device name.
-                        ProjectionChipModel.Projecting(type, state, deviceName = null)
+                        ProjectionChipModel.Projecting(type, state)
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
index a1a5e82..85682f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
@@ -26,16 +26,10 @@
     /** There is no media being projected. */
     data object NotProjecting : ProjectionChipModel()
 
-    /**
-     * Media is currently being projected.
-     *
-     * @property deviceName the name of the device receiving the projection, or null if the
-     *   projection is to this device (as opposed to a different device).
-     */
+    /** Media is currently being projected. */
     data class Projecting(
         val type: Type,
         val projectionState: MediaProjectionState.Projecting,
-        val deviceName: String?,
     ) : ProjectionChipModel()
 
     enum class Type {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index df25d57..e201652 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -19,7 +19,6 @@
 import android.app.ActivityManager
 import android.content.Context
 import androidx.annotation.DrawableRes
-import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -52,7 +51,6 @@
     private val interactor: ScreenRecordChipInteractor,
     private val systemClock: SystemClock,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
 ) : OngoingActivityChipViewModel {
     override val chip: StateFlow<OngoingActivityChipModel> =
         interactor.screenRecordState
@@ -78,7 +76,6 @@
                             startTimeMs = systemClock.elapsedRealtime(),
                             createDialogLaunchOnClickListener(
                                 createDelegate(state.recordedTask),
-                                dialogTransitionAnimator,
                             ),
                         )
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index c097720..45260e18 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import androidx.annotation.DrawableRes
-import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -52,7 +51,6 @@
     private val context: Context,
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val systemClock: SystemClock,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
 ) : OngoingActivityChipViewModel {
     override val chip: StateFlow<OngoingActivityChipModel> =
@@ -89,10 +87,7 @@
             colors = ColorsModel.Red,
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
             startTimeMs = systemClock.elapsedRealtime(),
-            createDialogLaunchOnClickListener(
-                createShareToAppDialogDelegate(state),
-                dialogTransitionAnimator
-            ),
+            createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state)),
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
index 0dbf5d6..65f94ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
@@ -17,10 +17,7 @@
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
 import android.view.View
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import kotlinx.coroutines.flow.StateFlow
 
@@ -36,19 +33,10 @@
         /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */
         fun createDialogLaunchOnClickListener(
             dialogDelegate: SystemUIDialog.Delegate,
-            dialogTransitionAnimator: DialogTransitionAnimator,
         ): View.OnClickListener {
             return View.OnClickListener { view ->
                 val dialog = dialogDelegate.createDialog()
-                val launchableView =
-                    view.requireViewById<ChipBackgroundContainer>(
-                        R.id.ongoing_activity_chip_background
-                    )
-                // TODO(b/343699052): This makes a beautiful animate-in, but the
-                //  animate-out looks odd because the dialog animates back into the chip
-                //  but then the chip disappears. If we aren't able to address
-                //  b/343699052 in time for launch, we should just use `dialog.show`.
-                dialogTransitionAnimator.showFromView(dialog, launchableView)
+                dialog.show()
             }
         }
     }
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 f98a88f..e48c28d 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
@@ -698,7 +698,7 @@
     }
 
     public void setHeadsUpIsVisible() {
-        if (row != null) row.setHeadsUpIsVisible();
+        if (row != null) row.markHeadsUpSeen();
     }
 
     //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index eebbb13..bf44b9f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@file:OptIn(ExperimentalCoroutinesApi::class)
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
@@ -25,17 +25,14 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
 
 class HeadsUpNotificationInteractor
 @Inject
@@ -50,48 +47,54 @@
     val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
 
     /** Set of currently pinned top-level heads up rows to be displayed. */
-    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> =
-        headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories ->
-            if (repositories.isNotEmpty()) {
-                val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
-                    repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } }
-                combine(toCombine) { pairs ->
-                    pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            headsUpRepository.activeHeadsUpRows.flatMapLatest { repositories ->
+                if (repositories.isNotEmpty()) {
+                    val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> =
+                        repositories.map { repo ->
+                            repo.isPinned.map { isPinned -> repo to isPinned }
+                        }
+                    combine(toCombine) { pairs ->
+                        pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+                    }
+                } else {
+                    // if the set is empty, there are no flows to combine
+                    flowOf(emptySet())
                 }
-            } else {
-                // if the set is empty, there are no flows to combine
-                flowOf(emptySet())
             }
         }
+    }
 
     /** Are there any pinned heads up rows to display? */
-    val hasPinnedRows: Flow<Boolean> =
-        headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
-            if (rows.isNotEmpty()) {
-                combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
-            } else {
-                // if the set is empty, there are no flows to combine
-                flowOf(false)
-            }
-        }
-
-    val isHeadsUpOrAnimatingAway: Flow<Boolean> =
-        combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-                hasPinnedRows,
-                animatingAway ->
-                hasPinnedRows || animatingAway
-            }
-            .debounce { isHeadsUpOrAnimatingAway ->
-                if (isHeadsUpOrAnimatingAway) {
-                    0
+    val hasPinnedRows: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            headsUpRepository.activeHeadsUpRows.flatMapLatest { rows ->
+                if (rows.isNotEmpty()) {
+                    combine(rows.map { it.isPinned }) { pins -> pins.any { it } }
                 } else {
-                    // When the last pinned entry is removed from the [HeadsUpRepository],
-                    // there might be a delay before the View starts animating.
-                    50L
+                    // if the set is empty, there are no flows to combine
+                    flowOf(false)
                 }
             }
-            .onStart { emit(false) } // emit false, so we don't wait for the initial update
-            .distinctUntilChanged()
+        }
+    }
+
+    val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
+                    hasPinnedRows,
+                    animatingAway ->
+                    hasPinnedRows || animatingAway
+                }
+        }
+    }
 
     private val canShowHeadsUp: Flow<Boolean> =
         combine(
@@ -109,10 +112,15 @@
             }
         }
 
-    val showHeadsUpStatusBar: Flow<Boolean> =
-        combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp ->
-            hasPinnedRows && canShowHeadsUp
+    val showHeadsUpStatusBar: Flow<Boolean> by lazy {
+        if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
+            flowOf(false)
+        } else {
+            combine(hasPinnedRows, canShowHeadsUp) { hasPinnedRows, canShowHeadsUp ->
+                hasPinnedRows && canShowHeadsUp
+            }
         }
+    }
 
     fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
         HeadsUpRowInteractor(key as HeadsUpRowRepository)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 582d847..1cbb16e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -859,8 +859,8 @@
     }
 
     @Override
-    public void setHeadsUpIsVisible() {
-        super.setHeadsUpIsVisible();
+    public void markHeadsUpSeen() {
+        super.markHeadsUpSeen();
         mMustStayOnScreen = false;
     }
 
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 2af119f..6becbd2 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
@@ -637,7 +637,10 @@
         return false;
     }
 
-    public void setHeadsUpIsVisible() {
+    /**
+     * Called, when the notification has been seen by the user in the heads up state.
+     */
+    public void markHeadsUpSeen() {
     }
 
     public boolean showingPulsing() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index b5ea861..b8af369 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.row
 
 import android.app.Notification
+import android.app.Notification.RichOngoingStyle
 import android.app.PendingIntent
 import android.content.Context
 import android.util.Log
@@ -68,12 +69,14 @@
         builder: Notification.Builder,
         systemUIContext: Context,
         packageContext: Context
-    ): RichOngoingContentModel? =
+    ): RichOngoingContentModel? {
+        if (builder.style !is RichOngoingStyle) return null
+
         try {
             val sbn = entry.sbn
             val notification = sbn.notification
             val icon = IconModel(notification.smallIcon)
-            if (sbn.packageName == "com.google.android.deskclock") {
+            return if (sbn.packageName == "com.google.android.deskclock") {
                 when (notification.channelId) {
                     "Timers v2" -> {
                         parseTimerNotification(notification, icon)
@@ -90,8 +93,9 @@
             } else null
         } catch (e: Exception) {
             Log.e("RONs", "Error parsing RON", e)
-            null
+            return null
         }
+    }
 
     /**
      * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
index d1e5ab0..83de226 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
@@ -176,7 +176,7 @@
             expandableView.setInShelf(inShelf);
 
             if (headsUpIsVisible) {
-                expandableView.setHeadsUpIsVisible();
+                expandableView.markHeadsUpSeen();
             }
         }
     }
@@ -231,7 +231,7 @@
         expandableView.setInShelf(this.inShelf);
 
         if (headsUpIsVisible) {
-            expandableView.setHeadsUpIsVisible();
+            expandableView.markHeadsUpSeen();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
index 52cb48b..8d73983 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -51,8 +51,8 @@
                             }
                             removed.forEach { key ->
                                 val row = obtainView(key)
-                                parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false)
-                                row.setHeadsUpIsVisible()
+                                parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false)
+                                row.markHeadsUpSeen()
                             }
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index a11cbc3..98869be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -37,6 +37,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.Dumpable;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -54,6 +55,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.util.Optional;
@@ -86,6 +88,7 @@
     private final FoldAodAnimationController mFoldAodAnimationController;
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     private final UserTracker mUserTracker;
+    private final SecureSettings mSecureSettings;
 
     private boolean mDozeAlwaysOn;
     private boolean mControlScreenOffAnimation;
@@ -130,7 +133,8 @@
             ConfigurationController configurationController,
             StatusBarStateController statusBarStateController,
             UserTracker userTracker,
-            DozeInteractor dozeInteractor) {
+            DozeInteractor dozeInteractor,
+            SecureSettings secureSettings) {
         mResources = resources;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
         mAlwaysOnPolicy = alwaysOnDisplayPolicy;
@@ -144,6 +148,7 @@
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
         mUserTracker = userTracker;
         mDozeInteractor = dozeInteractor;
+        mSecureSettings = secureSettings;
 
         keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback);
         tunerService.addTunable(
@@ -160,7 +165,8 @@
             mFoldAodAnimationController.addCallback(this);
         }
 
-        SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler);
+        SettingsObserver quickPickupSettingsObserver =
+                new SettingsObserver(context, handler, mSecureSettings);
         quickPickupSettingsObserver.observe();
 
         batteryController.addCallback(new BatteryStateChangeCallback() {
@@ -479,18 +485,36 @@
                 Settings.Secure.getUriFor(Settings.Secure.DOZE_ALWAYS_ON);
         private final Context mContext;
 
-        SettingsObserver(Context context, Handler handler) {
+        private final Handler mHandler;
+        private final SecureSettings mSecureSettings;
+
+        SettingsObserver(Context context, Handler handler, SecureSettings secureSettings) {
             super(handler);
             mContext = context;
+            mHandler = handler;
+            mSecureSettings = secureSettings;
         }
 
         void observe() {
-            ContentResolver resolver = mContext.getContentResolver();
-            resolver.registerContentObserver(mQuickPickupGesture, false, this,
-                    UserHandle.USER_ALL);
-            resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);
-            resolver.registerContentObserver(mAlwaysOnEnabled, false, this, UserHandle.USER_ALL);
-            update(null);
+            if (Flags.registerContentObserversAsync()) {
+                mSecureSettings.registerContentObserverForUserAsync(mQuickPickupGesture,
+                        this, UserHandle.USER_ALL);
+                mSecureSettings.registerContentObserverForUserAsync(mPickupGesture,
+                        this, UserHandle.USER_ALL);
+                mSecureSettings.registerContentObserverForUserAsync(mAlwaysOnEnabled,
+                        this, UserHandle.USER_ALL,
+                        // The register calls are called in order, so this ensures that update()
+                        // is called after them all and value retrieval isn't racy.
+                        () -> mHandler.post(() -> update(null)));
+            } else {
+                ContentResolver resolver = mContext.getContentResolver();
+                resolver.registerContentObserver(mQuickPickupGesture, false, this,
+                        UserHandle.USER_ALL);
+                resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);
+                resolver.registerContentObserver(mAlwaysOnEnabled, false, this,
+                        UserHandle.USER_ALL);
+                update(null);
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 507759c..68163b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -325,7 +325,7 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), true)
 
-    private val shownLevel: StateFlow<Int> =
+    private val cellularShownLevel: StateFlow<Int> =
         combine(
                 level,
                 isInService,
@@ -337,15 +337,19 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
+    // Satellite level is unaffected by the isInService or inflateSignalStrength properties
+    // See b/346904529 for details
+    private val satelliteShownLevel: StateFlow<Int> = level
+
     private val cellularIcon: Flow<SignalIconModel.Cellular> =
         combine(
-            shownLevel,
+            cellularShownLevel,
             numberOfLevels,
             showExclamationMark,
             carrierNetworkChangeActive,
-        ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
+        ) { cellularShownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
             SignalIconModel.Cellular(
-                shownLevel,
+                cellularShownLevel,
                 numberOfLevels,
                 showExclamationMark,
                 carrierNetworkChange,
@@ -353,7 +357,7 @@
         }
 
     private val satelliteIcon: Flow<SignalIconModel.Satellite> =
-        shownLevel.map {
+        satelliteShownLevel.map {
             SignalIconModel.Satellite(
                 level = it,
                 icon =
@@ -365,7 +369,7 @@
     override val signalLevelIcon: StateFlow<SignalIconModel> = run {
         val initial =
             SignalIconModel.Cellular(
-                shownLevel.value,
+                cellularShownLevel.value,
                 numberOfLevels.value,
                 showExclamationMark.value,
                 carrierNetworkChangeActive.value,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 2ce2bc8..81e41d67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -45,6 +45,10 @@
 import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileDataInteractor
 import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
 import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor
 import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.model.SensorPrivacyToggleTileModel
@@ -85,35 +89,35 @@
     @IntoMap
     @StringKey(FLASHLIGHT_TILE_SPEC)
     fun provideAirplaneModeAvailabilityInteractor(
-            impl: FlashlightTileDataInteractor
+        impl: FlashlightTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(LOCATION_TILE_SPEC)
     fun provideLocationAvailabilityInteractor(
-            impl: LocationTileDataInteractor
+        impl: LocationTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(ALARM_TILE_SPEC)
     fun provideAlarmAvailabilityInteractor(
-            impl: AlarmTileDataInteractor
+        impl: AlarmTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(UIMODENIGHT_TILE_SPEC)
     fun provideUiModeNightAvailabilityInteractor(
-            impl: UiModeNightTileDataInteractor
+        impl: UiModeNightTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(WORK_MODE_TILE_SPEC)
     fun provideWorkModeAvailabilityInteractor(
-            impl: WorkModeTileDataInteractor
+        impl: WorkModeTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     companion object {
@@ -125,6 +129,7 @@
         const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle"
         const val MIC_TOGGLE_TILE_SPEC = "mictoggle"
         const val DND_TILE_SPEC = "dnd"
+        const val MODES_TILE_SPEC = "modes"
 
         /** Inject flashlight config */
         @Provides
@@ -327,7 +332,7 @@
         @IntoMap
         @StringKey(CAMERA_TOGGLE_TILE_SPEC)
         fun provideCameraToggleAvailabilityInteractor(
-                factory: SensorPrivacyToggleTileDataInteractor.Factory
+            factory: SensorPrivacyToggleTileDataInteractor.Factory
         ): QSTileAvailabilityInteractor {
             return factory.create(CAMERA)
         }
@@ -369,12 +374,11 @@
         @IntoMap
         @StringKey(MIC_TOGGLE_TILE_SPEC)
         fun provideMicToggleModeAvailabilityInteractor(
-                factory: SensorPrivacyToggleTileDataInteractor.Factory
+            factory: SensorPrivacyToggleTileDataInteractor.Factory
         ): QSTileAvailabilityInteractor {
             return factory.create(MICROPHONE)
         }
 
-
         /** Inject microphone toggle config */
         @Provides
         @IntoMap
@@ -389,6 +393,37 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
             )
+
+        @Provides
+        @IntoMap
+        @StringKey(MODES_TILE_SPEC)
+        fun provideModesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(MODES_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.qs_dnd_icon_off,
+                        labelRes = R.string.quick_settings_modes_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /** Inject ModesTile into tileViewModelMap in QSModule */
+        @Provides
+        @IntoMap
+        @StringKey(MODES_TILE_SPEC)
+        fun provideModesTileViewModel(
+            factory: QSTileViewModelFactory.Static<ModesTileModel>,
+            mapper: ModesTileMapper,
+            stateInteractor: ModesTileDataInteractor,
+            userActionInteractor: ModesTileUserActionInteractor
+        ): QSTileViewModel =
+            factory.create(
+                TileSpec.create(MODES_TILE_SPEC),
+                userActionInteractor,
+                stateInteractor,
+                mapper,
+            )
     }
 
     /** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java b/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java
deleted file mode 100644
index d54c07c..0000000
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunablePadding.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2017 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.tuner;
-
-import android.util.DisplayMetrics;
-import android.view.View;
-import android.view.WindowManager;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.tuner.TunerService.Tunable;
-
-import javax.inject.Inject;
-
-/**
- * Version of Space that can be resized by a tunable setting.
- */
-public class TunablePadding implements Tunable {
-
-    public static final int FLAG_START = 1;
-    public static final int FLAG_END = 2;
-    public static final int FLAG_TOP = 4;
-    public static final int FLAG_BOTTOM = 8;
-
-    private final int mFlags;
-    private final View mView;
-    private final int mDefaultSize;
-    private final float mDensity;
-    private final TunerService mTunerService;
-
-    private TunablePadding(String key, int def, int flags, View view, TunerService tunerService) {
-        mDefaultSize = def;
-        mFlags = flags;
-        mView = view;
-        DisplayMetrics metrics = new DisplayMetrics();
-        view.getContext().getSystemService(WindowManager.class)
-                .getDefaultDisplay().getMetrics(metrics);
-        mDensity = metrics.density;
-        mTunerService = tunerService;
-        mTunerService.addTunable(this, key);
-    }
-
-    @Override
-    public void onTuningChanged(String key, String newValue) {
-        int dimen = mDefaultSize;
-        if (newValue != null) {
-            try {
-                dimen = (int) (Integer.parseInt(newValue) * mDensity);
-            } catch (NumberFormatException ex) {}
-        }
-        int left = mView.isLayoutRtl() ? FLAG_END : FLAG_START;
-        int right = mView.isLayoutRtl() ? FLAG_START : FLAG_END;
-        mView.setPadding(getPadding(dimen, left), getPadding(dimen, FLAG_TOP),
-                getPadding(dimen, right), getPadding(dimen, FLAG_BOTTOM));
-    }
-
-    private int getPadding(int dimen, int flag) {
-        return ((mFlags & flag) != 0) ? dimen : 0;
-    }
-
-    public void destroy() {
-        mTunerService.removeTunable(this);
-    }
-
-    /**
-     * Exists for easy injecting in tests.
-     */
-    @SysUISingleton
-    public static class TunablePaddingService {
-
-        private final TunerService mTunerService;
-
-        /**
-         */
-        @Inject
-        public TunablePaddingService(TunerService tunerService) {
-            mTunerService = tunerService;
-        }
-
-        public TunablePadding add(View view, String key, int defaultSize, int flags) {
-            if (view == null) {
-                throw new IllegalArgumentException();
-            }
-            return new TunablePadding(key, defaultSize, flags, view, mTunerService);
-        }
-    }
-
-    public static TunablePadding addTunablePadding(View view, String key, int defaultSize,
-            int flags) {
-        return Dependency.get(TunablePaddingService.class).add(view, key, defaultSize, flags);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt
index ec7aabb..f2132248 100644
--- a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt
@@ -20,6 +20,7 @@
 import android.util.IndentingPrintWriter
 import android.view.View
 import android.view.ViewGroup
+import dagger.Lazy
 import java.io.PrintWriter
 
 /** [Sequence] that yields all of the direct children of this [ViewGroup] */
@@ -56,3 +57,8 @@
         getBoundsOnScreen(bounds)
         return bounds
     }
+
+/** Extension method to convert [dagger.Lazy] to [kotlin.Lazy] for object of any class [T]. */
+fun <T> Lazy<T>.toKotlinLazy(): kotlin.Lazy<T> {
+    return lazy { this.get() }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 3bf5b65..025354b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -21,6 +21,7 @@
 import android.net.Uri
 import android.os.UserHandle
 import android.provider.Settings.SettingNotFoundException
+import androidx.annotation.WorkerThread
 import com.android.app.tracing.TraceUtils.trace
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat
@@ -199,6 +200,24 @@
         }
 
     /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver].'
+     *
+     * API corresponding to [registerContentObserverForUser] for Java usage. After registration is
+     * complete, the callback block is called on the <b>background thread</b> to allow for update of
+     * value.
+     */
+    fun registerContentObserverForUserAsync(
+        uri: Uri,
+        settingsObserver: ContentObserver,
+        userHandle: Int,
+        @WorkerThread registered: Runnable
+    ) =
+        CoroutineScope(backgroundDispatcher).launch {
+            registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+            registered.run()
+        }
+
+    /**
      * Convenience wrapper around [ContentResolver.registerContentObserver]
      *
      * Implicitly calls [getUriFor] on the passed in name.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
index bd698ab..bb230e6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/CsdWarningDialog.java
@@ -30,21 +30,31 @@
 import android.media.AudioManager;
 import android.provider.Settings;
 import android.util.Log;
+import android.util.Pair;
 import android.view.KeyEvent;
 import android.view.WindowManager;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.systemui.Flags;
+import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.NotificationChannels;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
+import com.google.common.collect.ImmutableList;
+
 import dagger.assisted.Assisted;
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
+import java.util.Optional;
+
 /**
  * A class that implements the three Computed Sound Dose-related warnings defined in
  * {@link AudioManager}:
@@ -75,6 +85,9 @@
     private static final int KEY_CONFIRM_ALLOWED_AFTER_MS = 1000; // milliseconds
     // time after which action is taken when the user hasn't ack'd or dismissed the dialog
     public static final int NO_ACTION_TIMEOUT_MS = 5000;
+    // Notification dismiss intent
+    private static final String DISMISS_CSD_NOTIFICATION =
+            "com.android.systemui.volume.DISMISS_CSD_NOTIFICATION";
 
     private final Context mContext;
     private final AudioManager mAudioManager;
@@ -95,21 +108,32 @@
 
     private long mShowTime;
 
+    @VisibleForTesting public int mCachedMediaStreamVolume;
+    private Optional<ImmutableList<Pair<String, Intent>>> mActionIntents;
+    private final BroadcastDispatcher mBroadcastDispatcher;
+
     /**
      * To inject dependencies and allow for easier testing
      */
     @AssistedFactory
     public interface Factory {
-        /**
-         * Create a dialog object
-         */
-        CsdWarningDialog create(int csdWarning, Runnable onCleanup);
+        /** Create a dialog object */
+        CsdWarningDialog create(
+                int csdWarning,
+                Runnable onCleanup,
+                Optional<ImmutableList<Pair<String, Intent>>> actionIntents);
     }
 
     @AssistedInject
-    public CsdWarningDialog(@Assisted @AudioManager.CsdWarning int csdWarning, Context context,
-            AudioManager audioManager, NotificationManager notificationManager,
-            @Background DelayableExecutor delayableExecutor, @Assisted Runnable onCleanup) {
+    public CsdWarningDialog(
+            @Assisted @AudioManager.CsdWarning int csdWarning,
+            Context context,
+            AudioManager audioManager,
+            NotificationManager notificationManager,
+            @Background DelayableExecutor delayableExecutor,
+            @Assisted Runnable onCleanup,
+            @Assisted Optional<ImmutableList<Pair<String, Intent>>> actionIntents,
+            BroadcastDispatcher broadcastDispatcher) {
         super(context);
         mCsdWarning = csdWarning;
         mContext = context;
@@ -118,7 +142,8 @@
         mOnCleanup = onCleanup;
 
         mDelayableExecutor = delayableExecutor;
-
+        mActionIntents = actionIntents;
+        mBroadcastDispatcher = broadcastDispatcher;
         getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
         setShowForAllUsers(true);
         setMessage(mContext.getString(getStringForWarning(csdWarning)));
@@ -133,14 +158,17 @@
                 Context.RECEIVER_EXPORTED_UNAUDITED);
 
         if (csdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) {
-            mNoUserActionRunnable = () -> {
-                if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) {
-                    // unlike on the 5x dose repeat, level is only reduced to RS1 when the warning
-                    // is not acknowledged quickly enough
-                    mAudioManager.lowerVolumeToRs1();
-                    sendNotification(/*for5XCsd=*/false);
-                }
-            };
+            mNoUserActionRunnable =
+                    () -> {
+                        if (mCsdWarning == AudioManager.CSD_WARNING_DOSE_REACHED_1X) {
+                            // unlike on the 5x dose repeat, level is only reduced to RS1 when the
+                            // warning is not acknowledged quickly enough
+                            mCachedMediaStreamVolume =
+                                    mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+                            mAudioManager.lowerVolumeToRs1();
+                            sendNotification(/* for5XCsd= */ false);
+                        }
+                    };
         } else {
             mNoUserActionRunnable = null;
         }
@@ -242,6 +270,38 @@
         }
     };
 
+    @VisibleForTesting
+    public final BroadcastReceiver mReceiverUndo =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (Flags.sounddoseCustomization()
+                            && VolumeDialog.ACTION_VOLUME_UNDO.equals(intent.getAction())) {
+                        if (D.BUG) Log.d(TAG, "Received ACTION_VOLUME_UNDO");
+                        mAudioManager.setStreamVolume(
+                                AudioManager.STREAM_MUSIC,
+                                mCachedMediaStreamVolume,
+                                AudioManager.FLAG_SHOW_UI);
+                        mNotificationManager.cancel(
+                                SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO);
+                        destroy();
+                    }
+                }
+            };
+
+    @VisibleForTesting
+    public final BroadcastReceiver mReceiverDismissNotification =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (Flags.sounddoseCustomization()
+                            && DISMISS_CSD_NOTIFICATION.equals(intent.getAction())) {
+                        if (D.BUG) Log.d(TAG, "Received DISMISS_CSD_NOTIFICATION");
+                        destroy();
+                    }
+                }
+            };
+
     private @StringRes int getStringForWarning(@AudioManager.CsdWarning int csdWarning) {
         switch (csdWarning) {
             case AudioManager.CSD_WARNING_DOSE_REACHED_1X:
@@ -259,7 +319,7 @@
             Log.w(TAG, "Notification dose repeat 5x is not shown for " + mCsdWarning);
             return;
         }
-
+        mCachedMediaStreamVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
         mAudioManager.lowerVolumeToRs1();
         sendNotification(/*for5XCsd=*/true);
     }
@@ -288,7 +348,62 @@
                         .setAutoCancel(true)
                         .setCategory(Notification.CATEGORY_SYSTEM);
 
+        if (Flags.sounddoseCustomization()
+                && mActionIntents.isPresent()
+                && !mActionIntents.get().isEmpty()) {
+            ImmutableList<Pair<String, Intent>> actionIntentsList = mActionIntents.get();
+            for (Pair<String, Intent> intentPair : actionIntentsList) {
+                if (intentPair != null && intentPair.first != null && intentPair.second != null) {
+                    PendingIntent pendingActionIntent =
+                            PendingIntent.getBroadcast(mContext, 0, intentPair.second,
+                                    FLAG_IMMUTABLE);
+                    builder.addAction(0, intentPair.first, pendingActionIntent);
+                    // Register receiver to undo volume only when
+                    // notification conaining the undo action would be sent.
+                    if (intentPair.first == mContext.getString(R.string.volume_undo_action)) {
+                        final IntentFilter filterUndo = new IntentFilter(
+                                VolumeDialog.ACTION_VOLUME_UNDO);
+                        mBroadcastDispatcher.registerReceiver(mReceiverUndo,
+                                filterUndo,
+                                /* executor = default */ null,
+                                /* user = default */ null,
+                                Context.RECEIVER_NOT_EXPORTED,
+                                /* permission = default */ null);
+
+                        // Register receiver to learn if notification has been dismissed.
+                        // This is required to unregister receivers to prevent leak.
+                        Intent dismissIntent = new Intent(DISMISS_CSD_NOTIFICATION)
+                                .setPackage(mContext.getPackageName());
+                        PendingIntent pendingDismissIntent = PendingIntent.getBroadcast(mContext,
+                                0, dismissIntent, FLAG_IMMUTABLE);
+                        mBroadcastDispatcher.registerReceiver(mReceiverDismissNotification,
+                                new IntentFilter(DISMISS_CSD_NOTIFICATION),
+                                /* executor = default */ null,
+                                /* user = default */ null,
+                                Context.RECEIVER_NOT_EXPORTED,
+                                /* permission = default */ null);
+                        builder.setDeleteIntent(pendingDismissIntent);
+                    }
+                }
+            }
+        }
+
         mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO,
                 builder.build());
     }
+
+    /**
+     * Unregister the Undo action receiver when the notification is dismissed.
+     * Unregister cannot happen when CsdWarning dialog is dismissed
+     * as the notification lifecycle would be longer.
+     */
+    @VisibleForTesting
+    public void destroy() {
+        if (Flags.sounddoseCustomization()
+                && mActionIntents.isPresent()
+                && !mActionIntents.get().isEmpty()) {
+            mBroadcastDispatcher.unregisterReceiver(mReceiverUndo);
+            mBroadcastDispatcher.unregisterReceiver(mReceiverDismissNotification);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 795d427..6b02e1a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -50,6 +50,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -77,6 +78,7 @@
 import android.provider.Settings.Global;
 import android.text.InputFilter;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.view.ContextThemeWrapper;
@@ -140,11 +142,14 @@
 import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
 
+import com.google.common.collect.ImmutableList;
+
 import dagger.Lazy;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Consumer;
 
 /**
@@ -315,6 +320,9 @@
     private final com.android.systemui.util.time.SystemClock mSystemClock;
     private final VolumePanelFlag mVolumePanelFlag;
     private final VolumeDialogInteractor mInteractor;
+    // Optional actions for soundDose
+    private Optional<ImmutableList<Pair<String, Intent>>> mCsdWarningNotificationActions =
+            Optional.of(ImmutableList.of());
 
     public VolumeDialogImpl(
             Context context,
@@ -2198,6 +2206,11 @@
         rescheduleTimeoutH();
     }
 
+    public void setCsdWarningNotificationActionIntents(
+            ImmutableList<Pair<String, Intent>> actionIntent) {
+        mCsdWarningNotificationActions = Optional.of(actionIntent);
+    }
+
     @VisibleForTesting void showCsdWarningH(int csdWarning, int durationMs) {
         synchronized (mSafetyWarningLock) {
 
@@ -2212,7 +2225,8 @@
                 recheckH(null);
             };
 
-            mCsdDialog = mCsdWarningDialogFactory.create(csdWarning, cleanUp);
+            mCsdDialog = mCsdWarningDialogFactory.create(
+                    csdWarning, cleanUp, mCsdWarningNotificationActions);
             mCsdDialog.show();
         }
         recheckH(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
index b23dfdc..8595178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java
@@ -21,6 +21,7 @@
 import android.graphics.Region;
 import android.os.IBinder;
 import android.view.Display;
+import android.view.KeyboardShortcutGroup;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -55,6 +56,11 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return mWindowManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+    }
+
+    @Override
     public Region getCurrentImeTouchRegion() {
         return mWindowManager.getCurrentImeTouchRegion();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index c30bedd..12140b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -288,7 +288,7 @@
         mockActivityQuery(true);
         mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit);
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mSpyContext).startActivity(intentCaptor.capture());
+        verify(mSpyContext).startActivityAsUser(intentCaptor.capture(), eq(UserHandle.CURRENT));
         assertThat(intentCaptor.getValue().getAction()).isEqualTo(
                 mMenuViewLayer.getIntentForEditScreen().getAction());
     }
@@ -299,6 +299,7 @@
         mockActivityQuery(false);
         mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit);
         verify(mSpyContext, never()).startActivity(any());
+        verify(mSpyContext, never()).startActivityAsUser(any(), any());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 8f7dc7cf..5ea5c21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -16,8 +16,9 @@
 
 package com.android.systemui.accessibility.hearingaid;
 
+import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+
 import static com.android.systemui.accessibility.hearingaid.HearingDevicesDialogDelegate.LIVE_CAPTION_INTENT;
-import static com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -26,9 +27,13 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.bluetooth.BluetoothProfile;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -42,6 +47,7 @@
 import android.testing.TestableLooper;
 import android.view.View;
 import android.widget.LinearLayout;
+import android.widget.Spinner;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -86,14 +92,15 @@
     public MockitoRule mockito = MockitoJUnit.rule();
 
     private static final String DEVICE_ADDRESS = "AA:BB:CC:DD:EE:FF";
+    private static final String DEVICE_NAME = "test_name";
     private static final String TEST_PKG = "pkg";
     private static final String TEST_CLS = "cls";
     private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PKG, TEST_CLS);
     private static final String TEST_LABEL = "label";
+    private static final int TEST_PRESET_INDEX = 1;
+    private static final String TEST_PRESET_NAME = "test_preset";
 
     @Mock
-    private SystemUIDialog.Factory mSystemUIDialogFactory;
-    @Mock
     private SystemUIDialogManager mSystemUIDialogManager;
     @Mock
     private SysUiState mSysUiState;
@@ -118,6 +125,8 @@
     @Mock
     private CachedBluetoothDevice mCachedDevice;
     @Mock
+    private BluetoothDevice mDevice;
+    @Mock
     private DeviceItem mHearingDeviceItem;
     @Mock
     private PackageManager mPackageManager;
@@ -125,7 +134,10 @@
     private ActivityInfo mActivityInfo;
     @Mock
     private Drawable mDrawable;
+    @Mock
+    private HearingDevicesPresetsController mPresetsController;
     private SystemUIDialog mDialog;
+    private SystemUIDialog.Factory mDialogFactory;
     private HearingDevicesDialogDelegate mDialogDelegate;
     private TestableLooper mTestableLooper;
     private final List<CachedBluetoothDevice> mDevices = new ArrayList<>();
@@ -141,23 +153,23 @@
         when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
         when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
         when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
+        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mCachedDevice.getDevice()).thenReturn(mDevice);
         when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
+        when(mCachedDevice.getName()).thenReturn(DEVICE_NAME);
+        when(mCachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true);
+        when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true);
+        when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true);
         when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice);
+
         mContext.setMockPackageManager(mPackageManager);
-
-        setUpPairNewDeviceDialog();
-
-        when(mSystemUIDialogFactory.create(any(SystemUIDialog.Delegate.class)))
-                .thenReturn(mDialog);
-    }
-
-    @Test
-    public void createDialog_dialogShown() {
-        assertThat(mDialogDelegate.createDialog()).isEqualTo(mDialog);
+        mDevices.add(mCachedDevice);
     }
 
     @Test
     public void clickPairNewDeviceButton_intentActionMatch() {
+        setUpPairNewDeviceDialog();
         mDialog.show();
 
         getPairNewDeviceButton(mDialog).performClick();
@@ -185,6 +197,7 @@
 
     @Test
     public void onDeviceItemOnClicked_connectedDevice_disconnect() {
+        setUpDeviceListDialog();
         when(mHearingDeviceItem.getType()).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE);
 
         mDialogDelegate.onDeviceItemOnClicked(mHearingDeviceItem, new View(mContext));
@@ -231,50 +244,100 @@
         assertThat(relatedToolsView.getChildCount()).isEqualTo(2);
     }
 
+    @Test
+    public void showDialog_noPreset_presetGone() {
+        when(mPresetsController.getAllPresetInfo()).thenReturn(new ArrayList<>());
+        when(mPresetsController.getActivePresetIndex()).thenReturn(PRESET_INDEX_UNAVAILABLE);
+
+        setUpDeviceListDialog();
+        mDialog.show();
+
+        Spinner spinner = (Spinner) getPresetSpinner(mDialog);
+        assertThat(spinner.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void showDialog_presetExist_presetSelected() {
+        BluetoothHapPresetInfo info = getTestPresetInfo();
+        when(mPresetsController.getAllPresetInfo()).thenReturn(List.of(info));
+        when(mPresetsController.getActivePresetIndex()).thenReturn(TEST_PRESET_INDEX);
+
+        setUpDeviceListDialog();
+        mDialog.show();
+
+        Spinner spinner = (Spinner) getPresetSpinner(mDialog);
+        assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(spinner.getSelectedItemPosition()).isEqualTo(0);
+    }
+
+    @Test
+    public void onActiveDeviceChanged_presetExist_presetSelected() {
+        setUpDeviceListDialog();
+        mDialog.show();
+        BluetoothHapPresetInfo info = getTestPresetInfo();
+        when(mPresetsController.getAllPresetInfo()).thenReturn(List.of(info));
+        when(mPresetsController.getActivePresetIndex()).thenReturn(TEST_PRESET_INDEX);
+
+        mDialogDelegate.onActiveDeviceChanged(mCachedDevice, BluetoothProfile.LE_AUDIO);
+        mTestableLooper.processAllMessages();
+
+        Spinner spinner = (Spinner) getPresetSpinner(mDialog);
+        assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(spinner.getSelectedItemPosition()).isEqualTo(0);
+    }
+
+
+
     private void setUpPairNewDeviceDialog() {
+        mDialogFactory = new SystemUIDialog.Factory(
+                mContext,
+                mSystemUIDialogManager,
+                mSysUiState,
+                getFakeBroadcastDispatcher(),
+                mDialogTransitionAnimator
+        );
         mDialogDelegate = new HearingDevicesDialogDelegate(
                 mContext,
                 true,
-                mSystemUIDialogFactory,
+                mDialogFactory,
                 mActivityStarter,
                 mDialogTransitionAnimator,
                 mLocalBluetoothManager,
                 new Handler(mTestableLooper.getLooper()),
                 mAudioManager
         );
-        mDialog = new SystemUIDialog(
-                mContext,
-                0,
-                DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                mSystemUIDialogManager,
-                mSysUiState,
-                getFakeBroadcastDispatcher(),
-                mDialogTransitionAnimator,
-                mDialogDelegate
-        );
+
+        mDialog = mDialogDelegate.createDialog();
     }
 
     private void setUpDeviceListDialog() {
+        mDialogFactory = new SystemUIDialog.Factory(
+                mContext,
+                mSystemUIDialogManager,
+                mSysUiState,
+                getFakeBroadcastDispatcher(),
+                mDialogTransitionAnimator
+        );
         mDialogDelegate = new HearingDevicesDialogDelegate(
                 mContext,
                 false,
-                mSystemUIDialogFactory,
+                mDialogFactory,
                 mActivityStarter,
                 mDialogTransitionAnimator,
                 mLocalBluetoothManager,
                 new Handler(mTestableLooper.getLooper()),
                 mAudioManager
         );
-        mDialog = new SystemUIDialog(
-                mContext,
-                0,
-                DEFAULT_DISMISS_ON_DEVICE_LOCK,
-                mSystemUIDialogManager,
-                mSysUiState,
-                getFakeBroadcastDispatcher(),
-                mDialogTransitionAnimator,
-                mDialogDelegate
-        );
+
+        mDialog = mDialogDelegate.createDialog();
+        mDialogDelegate.setHearingDevicesPresetsController(mPresetsController);
+    }
+
+    private BluetoothHapPresetInfo getTestPresetInfo() {
+        BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class);
+        when(info.getName()).thenReturn(TEST_PRESET_NAME);
+        when(info.getIndex()).thenReturn(TEST_PRESET_INDEX);
+        return info;
     }
 
     private View getPairNewDeviceButton(SystemUIDialog dialog) {
@@ -285,6 +348,10 @@
         return dialog.requireViewById(R.id.related_tools_container);
     }
 
+    private View getPresetSpinner(SystemUIDialog dialog) {
+        return dialog.requireViewById(R.id.preset_spinner);
+    }
+
     @After
     public void reset() {
         if (mDialogDelegate != null) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
index 7094848..e60848b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt
@@ -120,11 +120,26 @@
 
     private fun setUpDatabase(): List<FakeWidgetMetadata> {
         return listOf(
-                FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3),
-                FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2),
-                FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1),
+                FakeWidgetMetadata(
+                    widgetId = 11,
+                    componentName = "com.android.fakePackage1/fakeWidget1",
+                    rank = 3,
+                    userSerialNumber = 0,
+                ),
+                FakeWidgetMetadata(
+                    widgetId = 12,
+                    componentName = "com.android.fakePackage2/fakeWidget2",
+                    rank = 2,
+                    userSerialNumber = 0,
+                ),
+                FakeWidgetMetadata(
+                    widgetId = 13,
+                    componentName = "com.android.fakePackage3/fakeWidget3",
+                    rank = 1,
+                    userSerialNumber = 10,
+                ),
             )
-            .onEach { dao.addWidget(it.widgetId, it.componentName, it.rank) }
+            .onEach { dao.addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber) }
     }
 
     private fun getBackupDataInputStream(): BackupDataInputStream {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
index cde7a0e..983a435 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
@@ -66,11 +66,28 @@
         // Set up database
         val expectedWidgets =
             listOf(
-                FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3),
-                FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2),
-                FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1),
+                FakeWidgetMetadata(
+                    widgetId = 11,
+                    componentName = "com.android.fakePackage1/fakeWidget1",
+                    rank = 3,
+                    userSerialNumber = 0,
+                ),
+                FakeWidgetMetadata(
+                    widgetId = 12,
+                    componentName = "com.android.fakePackage2/fakeWidget2",
+                    rank = 2,
+                    userSerialNumber = 0,
+                ),
+                FakeWidgetMetadata(
+                    widgetId = 13,
+                    componentName = "com.android.fakePackage3/fakeWidget3",
+                    rank = 1,
+                    userSerialNumber = 10,
+                ),
             )
-        expectedWidgets.forEach { dao.addWidget(it.widgetId, it.componentName, it.rank) }
+        expectedWidgets.forEach {
+            dao.addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber)
+        }
 
         // Get communal hub state
         val state = underTest.getCommunalHubState()
@@ -128,7 +145,12 @@
         assertThat(underTest.fileExists()).isFalse()
     }
 
-    data class FakeWidgetMetadata(val widgetId: Int, val componentName: String, val rank: Int)
+    data class FakeWidgetMetadata(
+        val widgetId: Int,
+        val componentName: String,
+        val rank: Int,
+        val userSerialNumber: Int,
+    )
 
     companion object {
         /**
@@ -140,7 +162,8 @@
                 { actual, expected ->
                     actual?.widgetId == expected?.widgetId &&
                         actual?.componentName == expected?.componentName &&
-                        actual?.rank == expected?.rank
+                        actual?.rank == expected?.rank &&
+                        actual?.userSerialNumber == expected?.userSerialNumber
                 },
                 "represents",
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
new file mode 100644
index 0000000..eb0ab78
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.communal.data.db
+
+import androidx.room.testing.MigrationTestHelper
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalDatabaseMigrationsTest : SysuiTestCase() {
+
+    @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule()
+
+    @get:Rule
+    val migrationTestHelper =
+        MigrationTestHelper(
+            InstrumentationRegistry.getInstrumentation(),
+            CommunalDatabase::class.java.canonicalName,
+        )
+
+    @Test
+    fun migrate1To2() {
+        // Create a communal database in version 1
+        val databaseV1 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 1)
+
+        // Populate some fake data
+        val fakeWidgetsV1 =
+            listOf(
+                FakeCommunalWidgetItemV1(1, "test_widget_1", 11),
+                FakeCommunalWidgetItemV1(2, "test_widget_2", 12),
+                FakeCommunalWidgetItemV1(3, "test_widget_3", 13),
+            )
+        databaseV1.insertWidgetsV1(fakeWidgetsV1)
+
+        // Verify fake widgets populated
+        databaseV1.verifyWidgetsV1(fakeWidgetsV1)
+
+        // Run migration and get database V2, the migration test helper verifies that the schema is
+        // updated correctly
+        val databaseV2 =
+            migrationTestHelper.runMigrationsAndValidate(
+                name = DATABASE_NAME,
+                version = 2,
+                validateDroppedTables = false,
+                CommunalDatabase.MIGRATION_1_2,
+            )
+
+        // Verify data is migrated correctly
+        databaseV2.verifyWidgetsV2(fakeWidgetsV1.map { it.getV2() })
+    }
+
+    private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
+        widgets.forEach { widget ->
+            execSQL(
+                "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " +
+                    "VALUES(${widget.widgetId}, '${widget.componentName}', ${widget.itemId})"
+            )
+        }
+    }
+
+    private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
+        val cursor = query("SELECT * FROM communal_widget_table")
+        assertThat(cursor.moveToFirst()).isTrue()
+
+        widgets.forEach { widget ->
+            assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+            assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+                .isEqualTo(widget.componentName)
+            assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+
+            cursor.moveToNext()
+        }
+
+        // Verify there is no more columns
+        assertThat(cursor.isAfterLast).isTrue()
+    }
+
+    private fun SupportSQLiteDatabase.verifyWidgetsV2(widgets: List<FakeCommunalWidgetItemV2>) {
+        val cursor = query("SELECT * FROM communal_widget_table")
+        assertThat(cursor.moveToFirst()).isTrue()
+
+        widgets.forEach { widget ->
+            assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+            assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+                .isEqualTo(widget.componentName)
+            assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+            assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number")))
+                .isEqualTo(widget.userSerialNumber)
+
+            cursor.moveToNext()
+        }
+
+        // Verify there is no more columns
+        assertThat(cursor.isAfterLast).isTrue()
+    }
+
+    /**
+     * Returns the expected data after migration from V1 to V2, which is simply that the new user
+     * serial number field is now set to [CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED].
+     */
+    private fun FakeCommunalWidgetItemV1.getV2(): FakeCommunalWidgetItemV2 {
+        return FakeCommunalWidgetItemV2(
+            widgetId,
+            componentName,
+            itemId,
+            CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED,
+        )
+    }
+
+    private data class FakeCommunalWidgetItemV1(
+        val widgetId: Int,
+        val componentName: String,
+        val itemId: Int,
+    )
+
+    private data class FakeCommunalWidgetItemV2(
+        val widgetId: Int,
+        val componentName: String,
+        val itemId: Int,
+        val userSerialNumber: Int,
+    )
+
+    companion object {
+        private const val DATABASE_NAME = "communal_db"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index f77c7a6..d670508 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -67,11 +67,12 @@
     @Test
     fun addWidget_readValueInDb() =
         testScope.runTest {
-            val (widgetId, provider, priority) = widgetInfo1
+            val (widgetId, provider, priority, userSerialNumber) = widgetInfo1
             communalWidgetDao.addWidget(
                 widgetId = widgetId,
                 provider = provider,
                 priority = priority,
+                userSerialNumber = userSerialNumber,
             )
             val entry = communalWidgetDao.getWidgetByIdNow(id = 1)
             assertThat(entry).isEqualTo(communalWidgetItemEntry1)
@@ -80,11 +81,12 @@
     @Test
     fun deleteWidget_notInDb_returnsFalse() =
         testScope.runTest {
-            val (widgetId, provider, priority) = widgetInfo1
+            val (widgetId, provider, priority, userSerialNumber) = widgetInfo1
             communalWidgetDao.addWidget(
                 widgetId = widgetId,
                 provider = provider,
                 priority = priority,
+                userSerialNumber = userSerialNumber,
             )
             assertThat(communalWidgetDao.deleteWidgetById(widgetId = 123)).isFalse()
         }
@@ -95,11 +97,12 @@
             val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
             val widgets = collectLastValue(communalWidgetDao.getWidgets())
             widgetsToAdd.forEach {
-                val (widgetId, provider, priority) = it
+                val (widgetId, provider, priority, userSerialNumber) = it
                 communalWidgetDao.addWidget(
                     widgetId = widgetId,
                     provider = provider,
                     priority = priority,
+                    userSerialNumber = userSerialNumber
                 )
             }
             assertThat(widgets())
@@ -118,11 +121,12 @@
             val widgets = collectLastValue(communalWidgetDao.getWidgets())
 
             widgetsToAdd.forEach {
-                val (widgetId, provider, priority) = it
+                val (widgetId, provider, priority, userSerialNumber) = it
                 communalWidgetDao.addWidget(
                     widgetId = widgetId,
                     provider = provider,
                     priority = priority,
+                    userSerialNumber = userSerialNumber,
                 )
             }
             assertThat(widgets())
@@ -144,11 +148,12 @@
             val widgets = collectLastValue(communalWidgetDao.getWidgets())
 
             widgetsToAdd.forEach {
-                val (widgetId, provider, priority) = it
+                val (widgetId, provider, priority, userSerialNumber) = it
                 communalWidgetDao.addWidget(
                     widgetId = widgetId,
                     provider = provider,
                     priority = priority,
+                    userSerialNumber = userSerialNumber,
                 )
             }
             assertThat(widgets())
@@ -180,11 +185,12 @@
             val widgets = collectLastValue(communalWidgetDao.getWidgets())
 
             existingWidgets.forEach {
-                val (widgetId, provider, priority) = it
+                val (widgetId, provider, priority, userSerialNumber) = it
                 communalWidgetDao.addWidget(
                     widgetId = widgetId,
                     provider = provider,
                     priority = priority,
+                    userSerialNumber = userSerialNumber,
                 )
             }
             assertThat(widgets())
@@ -212,6 +218,7 @@
                 widgetId = widgetInfo3.widgetId,
                 provider = widgetInfo3.provider,
                 priority = 2,
+                userSerialNumber = widgetInfo3.userSerialNumber,
             )
             assertThat(widgets())
                 .containsExactly(
@@ -246,6 +253,7 @@
                         widgetId = fakeWidget.widgetId,
                         componentName = fakeWidget.componentName,
                         itemId = rank.uid,
+                        userSerialNumber = fakeWidget.userSerialNumber,
                     )
                 expected[rank] = widget
             }
@@ -258,13 +266,15 @@
             widgetId = metadata.widgetId,
             provider = metadata.provider,
             priority = priority ?: metadata.priority,
+            userSerialNumber = metadata.userSerialNumber,
         )
     }
 
     data class FakeWidgetMetadata(
         val widgetId: Int,
         val provider: ComponentName,
-        val priority: Int
+        val priority: Int,
+        val userSerialNumber: Int,
     )
 
     companion object {
@@ -272,19 +282,22 @@
             FakeWidgetMetadata(
                 widgetId = 1,
                 provider = ComponentName("pk_name", "cls_name_1"),
-                priority = 1
+                priority = 1,
+                userSerialNumber = 0,
             )
         val widgetInfo2 =
             FakeWidgetMetadata(
                 widgetId = 2,
                 provider = ComponentName("pk_name", "cls_name_2"),
-                priority = 2
+                priority = 2,
+                userSerialNumber = 0,
             )
         val widgetInfo3 =
             FakeWidgetMetadata(
                 widgetId = 3,
                 provider = ComponentName("pk_name", "cls_name_3"),
-                priority = 3
+                priority = 3,
+                userSerialNumber = 10,
             )
         val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority)
         val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority)
@@ -295,6 +308,7 @@
                 widgetId = widgetInfo1.widgetId,
                 componentName = widgetInfo1.provider.flattenToString(),
                 itemId = communalItemRankEntry1.uid,
+                userSerialNumber = widgetInfo1.userSerialNumber,
             )
         val communalWidgetItemEntry2 =
             CommunalWidgetItem(
@@ -302,6 +316,7 @@
                 widgetId = widgetInfo2.widgetId,
                 componentName = widgetInfo2.provider.flattenToString(),
                 itemId = communalItemRankEntry2.uid,
+                userSerialNumber = widgetInfo2.userSerialNumber,
             )
         val communalWidgetItemEntry3 =
             CommunalWidgetItem(
@@ -309,6 +324,7 @@
                 widgetId = widgetInfo3.widgetId,
                 componentName = widgetInfo3.provider.flattenToString(),
                 itemId = communalItemRankEntry3.uid,
+                userSerialNumber = widgetInfo3.userSerialNumber,
             )
         val fakeState =
             CommunalHubState().apply {
@@ -318,11 +334,13 @@
                                 widgetId = 1
                                 componentName = "pk_name/fake_widget_1"
                                 rank = 1
+                                userSerialNumber = 0
                             },
                             CommunalHubState.CommunalWidgetItem().apply {
                                 widgetId = 2
                                 componentName = "pk_name/fake_widget_2"
                                 rank = 2
+                                userSerialNumber = 10
                             },
                         )
                         .toTypedArray()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..14837f2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.keyboard.shortcut.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
+    private val kosmos =
+        testKosmos().also {
+            it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
+        }
+
+    private val repo = kosmos.shortcutHelperCategoriesRepository
+    private val helper = kosmos.shortcutHelperTestHelper
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
+    @Test
+    fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+            fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+            helper.showFromActivity()
+            val firstCategories by collectLastValue(repo.categories)
+
+            // Intentionally change shortcuts now. This simulates "current app" shortcuts changing
+            // when our helper is shown.
+            // We still want to return the shortcuts that were returned before our helper was
+            // showing.
+            fakeSystemSource.setGroups(emptyList())
+
+            val secondCategories by collectLastValue(repo.categories)
+            // Make sure the second subscriber receives the same value as the first subscriber, even
+            // though fetching shortcuts again would have returned a new result.
+            assertThat(secondCategories).isEqualTo(firstCategories)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index 765cd01..4fba7e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -220,7 +220,7 @@
     val imeGroups = listOf(standardGroup1, standardGroup2, standardGroup3)
     val imeCategory =
         ShortcutCategory(
-            type = ShortcutCategoryType.IME,
+            type = ShortcutCategoryType.InputMethodEditor,
             subCategories =
                 listOf(
                     subCategoryForInputLanguageSwitchShortcuts,
@@ -233,14 +233,14 @@
     val systemGroups = listOf(standardGroup3, standardGroup2, standardGroup1)
     val systemCategory =
         ShortcutCategory(
-            type = ShortcutCategoryType.SYSTEM,
+            type = ShortcutCategoryType.System,
             subCategories = listOf(standardSubCategory3, standardSubCategory2, standardSubCategory1)
         )
 
     val multitaskingGroups = listOf(standardGroup2, standardGroup1)
     val multitaskingCategory =
         ShortcutCategory(
-            type = ShortcutCategoryType.MULTI_TASKING,
+            type = ShortcutCategoryType.MultiTasking,
             subCategories = listOf(standardSubCategory2, standardSubCategory1)
         )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
index 4c1e869..57c8b44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
@@ -23,11 +23,12 @@
 import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
 import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.IME
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MULTI_TASKING
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.SYSTEM
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
 import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperCategoriesInteractor
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
@@ -48,14 +49,14 @@
 
     private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource()
     private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource()
-    private val defaultAppsShortcutsSource = FakeKeyboardShortcutGroupsSource()
     @OptIn(ExperimentalCoroutinesApi::class)
     private val kosmos =
         testKosmos().also {
             it.testDispatcher = UnconfinedTestDispatcher()
             it.shortcutHelperSystemShortcutsSource = systemShortcutsSource
             it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource
-            it.shortcutHelperAppCategoriesShortcutsSource = defaultAppsShortcutsSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
@@ -117,7 +118,7 @@
                     TestShortcuts.systemCategory,
                     TestShortcuts.multitaskingCategory,
                     ShortcutCategory(
-                        type = IME,
+                        type = InputMethodEditor,
                         subCategories =
                             TestShortcuts.imeSubCategoriesWithGroupedDuplicatedShortcutLabels
                     ),
@@ -137,7 +138,7 @@
             assertThat(categories)
                 .containsExactly(
                     ShortcutCategory(
-                        type = SYSTEM,
+                        type = System,
                         subCategories =
                             TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels
                     ),
@@ -160,7 +161,7 @@
                 .containsExactly(
                     TestShortcuts.systemCategory,
                     ShortcutCategory(
-                        type = MULTI_TASKING,
+                        type = MultiTasking,
                         subCategories =
                             TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels
                     ),
@@ -182,7 +183,7 @@
                     TestShortcuts.systemCategory,
                     TestShortcuts.multitaskingCategory,
                     ShortcutCategory(
-                        type = IME,
+                        type = InputMethodEditor,
                         subCategories =
                             TestShortcuts.imeSubCategoriesWithUnsupportedModifiersRemoved
                     ),
@@ -201,7 +202,7 @@
             assertThat(categories)
                 .containsExactly(
                     ShortcutCategory(
-                        type = SYSTEM,
+                        type = System,
                         subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved
                     ),
                     TestShortcuts.multitaskingCategory,
@@ -222,7 +223,7 @@
                 .containsExactly(
                     TestShortcuts.systemCategory,
                     ShortcutCategory(
-                        type = MULTI_TASKING,
+                        type = MultiTasking,
                         subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved
                     ),
                     TestShortcuts.imeCategory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
index 0757ea1..f8e2f47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -20,8 +20,15 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
 import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
 import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
 import com.android.systemui.kosmos.Kosmos
@@ -32,6 +39,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -40,10 +48,18 @@
 @RunWith(AndroidJUnit4::class)
 class ShortcutHelperActivityStarterTest : SysuiTestCase() {
 
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
     private val kosmos =
         Kosmos().also {
             it.testCase = this
             it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
@@ -51,6 +67,12 @@
     private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
     private val starter = kosmos.shortcutHelperActivityStarter
 
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
     @Test
     fun start_doesNotStartByDefault() =
         testScope.runTest {
@@ -70,13 +92,30 @@
         }
 
     @Test
+    fun start_onToggle_noShortcuts_doesNotStartActivity() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(emptyList())
+            fakeMultiTaskingSource.setGroups(emptyList())
+
+            starter.start()
+
+            testHelper.toggle(deviceId = 456)
+
+            assertThat(fakeStartActivity.startIntents).isEmpty()
+        }
+
+    @Test
     fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
         testScope.runTest {
             starter.start()
 
+            // Starts
             testHelper.toggle(deviceId = 456)
+            // Stops
             testHelper.toggle(deviceId = 456)
+            // Starts again
             testHelper.toggle(deviceId = 456)
+            // Stops
             testHelper.toggle(deviceId = 456)
 
             verifyShortcutHelperActivityStarted(numTimes = 2)
@@ -93,6 +132,18 @@
         }
 
     @Test
+    fun start_onRequestShowShortcuts_noShortcuts_doesNotStartActivity() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(emptyList())
+            fakeMultiTaskingSource.setGroups(emptyList())
+            starter.start()
+
+            testHelper.showFromActivity()
+
+            assertThat(fakeStartActivity.startIntents).isEmpty()
+        }
+
+    @Test
     fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
         testScope.runTest {
             starter.start()
@@ -109,13 +160,21 @@
         testScope.runTest {
             starter.start()
 
+            // No-op. Already hidden.
             testHelper.hideFromActivity()
+            // No-op. Already hidden.
             testHelper.hideForSystem()
+            // Show 1st time.
             testHelper.toggle(deviceId = 987)
+            // No-op. Already shown.
             testHelper.showFromActivity()
+            // Hidden.
             testHelper.hideFromActivity()
+            // No-op. Already hidden.
             testHelper.hideForSystem()
+            // Show 2nd time.
             testHelper.toggle(deviceId = 456)
+            // No-op. Already shown.
             testHelper.showFromActivity()
 
             verifyShortcutHelperActivityStarted(numTimes = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
index 80d487c..07feaa1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -21,6 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
+import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
+import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
 import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
 import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState
@@ -34,6 +41,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -42,10 +50,18 @@
 @RunWith(AndroidJUnit4::class)
 class ShortcutHelperViewModelTest : SysuiTestCase() {
 
+    private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
+    private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
+
     private val kosmos =
         Kosmos().also {
             it.testCase = this
             it.testDispatcher = UnconfinedTestDispatcher()
+            it.shortcutHelperSystemShortcutsSource = fakeSystemSource
+            it.shortcutHelperMultiTaskingShortcutsSource = fakeMultiTaskingSource
+            it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource()
+            it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
     private val testScope = kosmos.testScope
@@ -53,6 +69,12 @@
     private val sysUiState = kosmos.sysUiState
     private val viewModel = kosmos.shortcutHelperViewModel
 
+    @Before
+    fun setUp() {
+        fakeSystemSource.setGroups(TestShortcuts.systemGroups)
+        fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups)
+    }
+
     @Test
     fun shouldShow_falseByDefault() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index c0d411b..5db8981 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -16,19 +16,25 @@
 
 package com.android.systemui.mediaprojection.data.repository
 
+import android.hardware.display.displayManager
 import android.media.projection.MediaProjectionInfo
 import android.os.Binder
+import android.os.Handler
 import android.os.UserHandle
 import android.view.ContentRecordingSession
+import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
 import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeTasksRepository
 import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
 import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
 import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
@@ -36,7 +42,9 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -47,6 +55,7 @@
 
     private val fakeMediaProjectionManager = kosmos.fakeMediaProjectionManager
     private val fakeActivityTaskManager = kosmos.fakeActivityTaskManager
+    private val displayManager = kosmos.displayManager
 
     private val repo = kosmos.realMediaProjectionRepository
 
@@ -139,6 +148,37 @@
         }
 
     @Test
+    fun mediaProjectionState_entireScreen_validVirtualDisplayId_hasHostDeviceName() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
+            session.virtualDisplayId = 45
+            val displayInfo = mock<Display>().apply { whenever(this.name).thenReturn("Test Name") }
+            whenever(displayManager.getDisplay(45)).thenReturn(displayInfo)
+
+            fakeMediaProjectionManager.dispatchOnSessionSet(session = session)
+
+            assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostDeviceName)
+                .isEqualTo("Test Name")
+        }
+
+    @Test
+    fun mediaProjectionState_entireScreen_invalidVirtualDisplayId_nullHostDeviceName() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
+            session.virtualDisplayId = 45
+            whenever(displayManager.getDisplay(45)).thenReturn(null)
+
+            fakeMediaProjectionManager.dispatchOnSessionSet(session = session)
+
+            assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostDeviceName)
+                .isNull()
+        }
+
+    @Test
     fun mediaProjectionState_sessionSet_taskWithToken_matchingRunningTask_emitsSingleTask() =
         testScope.runTest {
             val token = createToken()
@@ -179,6 +219,89 @@
         }
 
     @Test
+    fun mediaProjectionState_singleTask_validVirtualDisplayId_hasHostDeviceName() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val token = createToken()
+            val task = createTask(taskId = 1, token = token)
+            fakeActivityTaskManager.addRunningTasks(task)
+
+            val session = ContentRecordingSession.createTaskSession(token.asBinder())
+            session.virtualDisplayId = 45
+            val displayInfo = mock<Display>().apply { whenever(this.name).thenReturn("Test Name") }
+            whenever(displayManager.getDisplay(45)).thenReturn(displayInfo)
+
+            fakeMediaProjectionManager.dispatchOnSessionSet(session = session)
+
+            assertThat((state as MediaProjectionState.Projecting.SingleTask).hostDeviceName)
+                .isEqualTo("Test Name")
+        }
+
+    @Test
+    fun mediaProjectionState_singleTask_invalidVirtualDisplayId_nullHostDeviceName() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val token = createToken()
+            val task = createTask(taskId = 1, token = token)
+            fakeActivityTaskManager.addRunningTasks(task)
+
+            val session = ContentRecordingSession.createTaskSession(token.asBinder())
+            session.virtualDisplayId = 45
+            whenever(displayManager.getDisplay(45)).thenReturn(null)
+
+            fakeMediaProjectionManager.dispatchOnSessionSet(session = session)
+
+            assertThat((state as MediaProjectionState.Projecting.SingleTask).hostDeviceName)
+                .isNull()
+        }
+
+    /** Regression test for b/352483752. */
+    @Test
+    fun mediaProjectionState_sessionStartedThenImmediatelyStopped_emitsOnlyNotProjecting() =
+        testScope.runTest {
+            val fakeTasksRepo = FakeTasksRepository()
+            val repoWithTimingControl =
+                MediaProjectionManagerRepository(
+                    // fakeTasksRepo lets us have control over when the background dispatcher
+                    // finishes fetching the tasks info.
+                    tasksRepository = fakeTasksRepo,
+                    mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
+                    displayManager = displayManager,
+                    handler = Handler.getMain(),
+                    applicationScope = kosmos.applicationCoroutineScope,
+                    backgroundDispatcher = kosmos.testDispatcher,
+                    mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+                )
+
+            val state by collectLastValue(repoWithTimingControl.mediaProjectionState)
+
+            val token = createToken()
+            val task = createTask(taskId = 1, token = token)
+
+            // Dispatch a session using a task session so that MediaProjectionManagerRepository
+            // has to ask TasksRepository for the tasks info.
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                session = ContentRecordingSession.createTaskSession(token.asBinder())
+            )
+            // FakeTasksRepository is set up to not return the tasks info until the test manually
+            // calls [FakeTasksRepository#setRunningTaskResult]. At this point,
+            // MediaProjectionManagerRepository is waiting for the tasks info and hasn't emitted
+            // anything yet.
+
+            // Before the tasks info comes back, dispatch a stop event.
+            fakeMediaProjectionManager.dispatchOnStop()
+
+            // Then let the tasks info come back.
+            fakeTasksRepo.setRunningTaskResult(task)
+
+            // Verify that MediaProjectionManagerRepository threw away the tasks info because
+            // a newer callback event (#onStop) occurred.
+            assertThat(state).isEqualTo(MediaProjectionState.NotProjecting)
+        }
+
+    @Test
     fun stopProjecting_invokesManager() =
         testScope.runTest {
             repo.stopProjecting()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
new file mode 100644
index 0000000..ce2b983
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeTasksRepository.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.mediaprojection.taskswitcher.data.repository
+
+import android.app.ActivityManager
+import android.os.IBinder
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/**
+ * Fake tasks repository that gives us fine-grained control over when the result of
+ * [findRunningTaskFromWindowContainerToken] gets emitted.
+ */
+class FakeTasksRepository : TasksRepository {
+    override suspend fun launchRecentTask(taskInfo: ActivityManager.RunningTaskInfo) {}
+
+    private val findRunningTaskResult: CompletableDeferred<ActivityManager.RunningTaskInfo?> =
+        CompletableDeferred()
+
+    override suspend fun findRunningTaskFromWindowContainerToken(
+        windowContainerToken: IBinder
+    ): ActivityManager.RunningTaskInfo? {
+        return findRunningTaskResult.await()
+    }
+
+    fun setRunningTaskResult(task: ActivityManager.RunningTaskInfo?) {
+        findRunningTaskResult.complete(task)
+    }
+
+    override val foregroundTask: Flow<ActivityManager.RunningTaskInfo> = emptyFlow()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
index 16a022f..8681123 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
@@ -276,8 +276,8 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mLauncherApps = mock(LauncherApps.class);
-        mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager,
-                mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager,
+        mManager = new PeopleSpaceWidgetManager(mContext, Optional.of(mAppWidgetManager),
+                mIPeopleManager, mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager,
                 Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager,
                 mNotificationManager, mFakeExecutor, mUserTracker);
         mManager.attach(mListenerService);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0b81b5e..8d3a29a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -69,7 +69,7 @@
 
     @Before
     fun setUp() {
-        whenever(controllerFactory.create(any(), any())).thenReturn(controller)
+        whenever(controllerFactory.create(any())).thenReturn(controller)
         whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
         whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
     }
@@ -83,8 +83,8 @@
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
-            verify(controllerFactory).create(eq(internalDisplay), any())
-            verify(controllerFactory, never()).create(eq(externalDisplay), any())
+            verify(controllerFactory).create(eq(internalDisplay))
+            verify(controllerFactory, never()).create(eq(externalDisplay))
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
@@ -118,8 +118,8 @@
                 callback
             )
 
-            verify(controllerFactory).create(eq(internalDisplay), any())
-            verify(controllerFactory, never()).create(eq(externalDisplay), any())
+            verify(controllerFactory).create(eq(internalDisplay))
+            verify(controllerFactory, never()).create(eq(externalDisplay))
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
@@ -151,7 +151,7 @@
     @Test
     fun executeScreenshots_allowedTypes_allCaptured() =
         testScope.runTest {
-            whenever(controllerFactory.create(any(), any())).thenReturn(controller)
+            whenever(controllerFactory.create(any())).thenReturn(controller)
 
             setDisplays(
                 display(TYPE_INTERNAL, id = 0),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
index 6985a27..63e56ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java
@@ -66,7 +66,9 @@
 
     @Before
     public void setUp() {
-        mKeyboardShortcutListSearch = new KeyboardShortcutListSearch(mContext, mWindowManager);
+        when(mWindowManager.getApplicationLaunchKeyboardShortcuts(anyInt())).thenReturn(
+                new KeyboardShortcutGroup("", Collections.emptyList()));
+        mKeyboardShortcutListSearch = new KeyboardShortcutListSearch(mContext, mWindowManager, -1);
         mKeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
         mKeyboardShortcutListSearch.mKeyboardShortcutsBottomSheetDialog = mBottomSheetDialog;
         mKeyboardShortcutListSearch.mContext = mContext;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
index 6ad8b8b..105cf16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java
@@ -54,6 +54,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 @SmallTest
@@ -71,6 +72,8 @@
 
     @Before
     public void setUp() {
+        when(mWindowManager.getApplicationLaunchKeyboardShortcuts(anyInt())).thenReturn(
+                new KeyboardShortcutGroup("", Collections.emptyList()));
         mKeyboardShortcuts = new KeyboardShortcuts(mContext, mWindowManager);
         KeyboardShortcuts.sInstance = mKeyboardShortcuts;
         mKeyboardShortcuts.mKeyboardShortcutsDialog = mDialog;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index c8397bd..5005d16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -75,7 +75,9 @@
 
     @Test
     fun message_entireScreen_unknownDevice() {
-        createAndSetDelegate(ENTIRE_SCREEN, deviceName = null)
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE, hostDeviceName = null)
+        )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
@@ -87,7 +89,12 @@
 
     @Test
     fun message_entireScreen_hasDevice() {
-        createAndSetDelegate(ENTIRE_SCREEN, deviceName = "My Favorite Device")
+        createAndSetDelegate(
+            MediaProjectionState.Projecting.EntireScreen(
+                HOST_PACKAGE,
+                hostDeviceName = "My Favorite Device"
+            )
+        )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
@@ -110,9 +117,9 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent)
             ),
-            deviceName = null,
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -133,9 +140,9 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
+                hostDeviceName = "My Favorite Device",
                 createTask(taskId = 1, baseIntent = baseIntent)
             ),
-            deviceName = "My Favorite Device",
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -161,9 +168,9 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent)
             ),
-            deviceName = null,
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -189,9 +196,9 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
-                createTask(taskId = 1, baseIntent = baseIntent)
+                hostDeviceName = "My Favorite Device",
+                createTask(taskId = 1, baseIntent = baseIntent),
             ),
-            deviceName = "My Favorite Device",
         )
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -240,10 +247,7 @@
             assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
         }
 
-    private fun createAndSetDelegate(
-        state: MediaProjectionState.Projecting,
-        deviceName: String? = null,
-    ) {
+    private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
         underTest =
             EndCastScreenToOtherDeviceDialogDelegate(
                 kosmos.endMediaProjectionDialogHelper,
@@ -252,15 +256,19 @@
                 ProjectionChipModel.Projecting(
                     ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE,
                     state,
-                    deviceName,
                 ),
             )
     }
 
     companion object {
         private const val HOST_PACKAGE = "fake.host.package"
-        private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
+        private val ENTIRE_SCREEN =
+            MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE, hostDeviceName = null)
         private val SINGLE_TASK =
-            MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                hostDeviceName = null,
+                createTask(taskId = 1)
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index fe29140..c9a7c82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -19,7 +19,6 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
@@ -38,7 +37,6 @@
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.policy.CastDevice
@@ -47,9 +45,7 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.mockito.ArgumentMatchers
 import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -65,17 +61,6 @@
     private val mockScreenCastDialog = mock<SystemUIDialog>()
     private val mockGenericCastDialog = mock<SystemUIDialog>()
 
-    private val chipBackgroundView = mock<ChipBackgroundContainer>()
-    private val chipView =
-        mock<View>().apply {
-            whenever(
-                    this.requireViewById<ChipBackgroundContainer>(
-                        R.id.ongoing_activity_chip_background
-                    )
-                )
-                .thenReturn(chipBackgroundView)
-        }
-
     private val underTest = kosmos.castToOtherDeviceChipViewModel
 
     @Before
@@ -116,6 +101,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
@@ -223,7 +209,11 @@
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
-                MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    hostDeviceName = null,
+                    createTask(taskId = 1),
+                )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
@@ -258,6 +248,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
@@ -306,14 +297,8 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockScreenCastDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            clickListener!!.onClick(mock<View>())
+            verify(mockScreenCastDialog).show()
         }
 
     @Test
@@ -324,20 +309,15 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockScreenCastDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            clickListener!!.onClick(mock<View>())
+            verify(mockScreenCastDialog).show()
         }
 
     @Test
@@ -359,13 +339,7 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockGenericCastDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            clickListener!!.onClick(mock<View>())
+            verify(mockGenericCastDialog).show()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index 7eb4ca0..d0c5e7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -72,6 +72,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1)
                 )
 
@@ -101,7 +102,11 @@
             val latest by collectLastValue(underTest.projection)
 
             mediaProjectionRepo.mediaProjectionState.value =
-                MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    hostDeviceName = null,
+                    createTask(taskId = 1),
+                )
 
             assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
             assertThat((latest as ProjectionChipModel.Projecting).type)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
index ab935fe..c62e404 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
@@ -72,6 +72,7 @@
         val projectionState =
             MediaProjectionState.Projecting.SingleTask(
                 "host.package",
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent),
             )
 
@@ -92,6 +93,7 @@
         val projectionState =
             MediaProjectionState.Projecting.SingleTask(
                 "host.package",
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent),
             )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 83b31c2..6bfb40f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -103,7 +103,11 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
             val task = createTask(taskId = 1)
             mediaProjectionRepo.mediaProjectionState.value =
-                MediaProjectionState.Projecting.SingleTask("host.package", task)
+                MediaProjectionState.Projecting.SingleTask(
+                    "host.package",
+                    hostDeviceName = null,
+                    task,
+                )
 
             assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index 0a06cc7..4728c64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -19,7 +19,6 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -34,7 +33,6 @@
 import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
@@ -42,9 +40,7 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.mockito.ArgumentMatchers
 import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -58,17 +54,6 @@
     private val systemClock = kosmos.fakeSystemClock
     private val mockSystemUIDialog = mock<SystemUIDialog>()
 
-    private val chipBackgroundView = mock<ChipBackgroundContainer>()
-    private val chipView =
-        mock<View>().apply {
-            whenever(
-                    this.requireViewById<ChipBackgroundContainer>(
-                        R.id.ongoing_activity_chip_background
-                    )
-                )
-                .thenReturn(chipBackgroundView)
-        }
-
     private val underTest = kosmos.screenRecordChipViewModel
 
     @Before
@@ -197,15 +182,9 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
+            clickListener!!.onClick(mock<View>())
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            verify(mockSystemUIDialog).show()
         }
 
     @Test
@@ -219,15 +198,9 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
+            clickListener!!.onClick(mock<View>())
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            verify(mockSystemUIDialog).show()
         }
 
     @Test
@@ -238,20 +211,15 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     "host.package",
+                    hostDeviceName = null,
                     FakeActivityTaskManager.createTask(taskId = 1)
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
+            clickListener!!.onClick(mock<View>())
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            verify(mockSystemUIDialog).show()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index bfb57c5..325a42b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -116,6 +116,7 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent)
             )
         )
@@ -140,6 +141,7 @@
         createAndSetDelegate(
             MediaProjectionState.Projecting.SingleTask(
                 HOST_PACKAGE,
+                hostDeviceName = null,
                 createTask(taskId = 1, baseIntent = baseIntent)
             )
         )
@@ -200,7 +202,6 @@
                 ProjectionChipModel.Projecting(
                     ProjectionChipModel.Type.SHARE_TO_APP,
                     state,
-                    deviceName = null,
                 ),
             )
     }
@@ -209,6 +210,10 @@
         private const val HOST_PACKAGE = "fake.host.package"
         private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
         private val SINGLE_TASK =
-            MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+            MediaProjectionState.Projecting.SingleTask(
+                HOST_PACKAGE,
+                hostDeviceName = null,
+                createTask(taskId = 1)
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 3028d00..f87b17d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -19,7 +19,6 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -35,7 +34,6 @@
 import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
@@ -43,9 +41,7 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.mockito.ArgumentMatchers
 import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -59,17 +55,6 @@
 
     private val mockShareDialog = mock<SystemUIDialog>()
 
-    private val chipBackgroundView = mock<ChipBackgroundContainer>()
-    private val chipView =
-        mock<View>().apply {
-            whenever(
-                    this.requireViewById<ChipBackgroundContainer>(
-                        R.id.ongoing_activity_chip_background
-                    )
-                )
-                .thenReturn(chipBackgroundView)
-        }
-
     private val underTest = kosmos.shareToAppChipViewModel
 
     @Before
@@ -98,6 +83,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     CAST_TO_OTHER_DEVICES_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
@@ -123,6 +109,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     NORMAL_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
@@ -176,6 +163,7 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     NORMAL_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
@@ -193,14 +181,8 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockShareDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            clickListener!!.onClick(mock<View>())
+            verify(mockShareDialog).show()
         }
 
     @Test
@@ -210,19 +192,14 @@
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     NORMAL_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(chipView)
-            verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockShareDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    ArgumentMatchers.anyBoolean(),
-                )
+            clickListener!!.onClick(mock<View>())
+            verify(mockShareDialog).show()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
index c9c7359..2e0c773 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
@@ -19,50 +19,25 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import kotlin.test.Test
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
 
 @SmallTest
 class OngoingActivityChipViewModelTest : SysuiTestCase() {
     private val mockSystemUIDialog = mock<SystemUIDialog>()
     private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog }
-    private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
-
-    private val chipBackgroundView = mock<ChipBackgroundContainer>()
-    private val chipView =
-        mock<View>().apply {
-            whenever(
-                    this.requireViewById<ChipBackgroundContainer>(
-                        R.id.ongoing_activity_chip_background
-                    )
-                )
-                .thenReturn(chipBackgroundView)
-        }
 
     @Test
     fun createDialogLaunchOnClickListener_showsDialogOnClick() {
-        val clickListener =
-            createDialogLaunchOnClickListener(dialogDelegate, dialogTransitionAnimator)
+        val clickListener = createDialogLaunchOnClickListener(dialogDelegate)
 
         // Dialogs must be created on the main thread
         context.mainExecutor.execute {
-            clickListener.onClick(chipView)
-            verify(dialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    eq(null),
-                    anyBoolean(),
-                )
+            clickListener.onClick(mock<View>())
+            verify(mockSystemUIDialog).show()
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 8bc83cf..b1a8d0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -155,6 +155,7 @@
             mediaProjectionState.value =
                 MediaProjectionState.Projecting.SingleTask(
                     NORMAL_PACKAGE,
+                    hostDeviceName = null,
                     createTask(taskId = 1),
                 )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
index 7cb41f1..10d07a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java
@@ -35,8 +35,8 @@
 import android.os.PowerManager;
 import android.provider.Settings;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
@@ -52,6 +52,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.unfold.FoldAodAnimationController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.util.settings.FakeSettings;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -130,7 +131,8 @@
             mConfigurationController,
             mStatusBarStateController,
             mUserTracker,
-            mDozeInteractor
+            mDozeInteractor,
+            new FakeSettings()
         );
 
         verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 30e96f1..e439aff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -738,6 +738,28 @@
             assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
         }
 
+    @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @Test
+    // See b/346904529 for more context
+    fun satBasedIcon_doesNotInflateSignalStrength() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+            // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+            connectionRepository.inflateSignalStrength.value = true
+
+            connectionRepository.primaryLevel.value = 4
+            assertThat(latest!!.level).isEqualTo(4)
+
+            connectionRepository.inflateSignalStrength.value = true
+            connectionRepository.primaryLevel.value = 4
+
+            // Icon level is unaffected
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
     private fun createInteractor(
         overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
     ) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index cec4155..e510924 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -862,6 +862,38 @@
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
         }
 
+    @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @Test
+    fun satelliteIcon_ignoresInflateSignalStrength() =
+        testScope.runTest {
+            // Note that this is the exact same test as above, but with inflateSignalStrength set to
+            // true we note that the level is unaffected by inflation
+            repository.inflateSignalStrength.value = true
+            repository.isNonTerrestrial.value = true
+            repository.setAllLevels(0)
+
+            val latest by
+                collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+            // Level 0 -> no connection
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+            // 1-2 -> 1 bar
+            repository.setAllLevels(1)
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            repository.setAllLevels(2)
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            // 3-4 -> 2 bars
+            repository.setAllLevels(3)
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+            repository.setAllLevels(4)
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+        }
+
     private fun createAndSetViewModel() {
         underTest =
             MobileIconViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index ef4e734..cc2ef53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -32,6 +33,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInteractor
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
 import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
@@ -126,6 +128,7 @@
         }
 
     @Test
+    @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
     fun isVisible_headsUpStatusBarShown_false() =
         testScope.runTest {
             val latest by collectLastValue(underTest.isVisible)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java b/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java
deleted file mode 100644
index bb7b31b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/tuner/TunablePaddingTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2017 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.tuner;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.LeakCheck.Tracker;
-import android.util.DisplayMetrics;
-import android.view.View;
-import android.view.WindowManager;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.utils.leaks.LeakCheckedTest;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TunablePaddingTest extends LeakCheckedTest {
-
-    private static final String KEY = "KEY";
-    private static final int DEFAULT = 42;
-    private View mView;
-    private TunablePadding mTunablePadding;
-    private TunerService mTunerService;
-
-    @Before
-    public void setup() {
-        injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
-        mView = mock(View.class);
-        when(mView.getContext()).thenReturn(mContext);
-
-        mTunerService = mock(TunerService.class);
-        mDependency.injectTestDependency(TunablePadding.TunablePaddingService.class,
-                new TunablePadding.TunablePaddingService(mTunerService));
-        Tracker tracker = mLeakCheck.getTracker("tuner");
-        doAnswer(invocation -> {
-            tracker.getLeakInfo(invocation.getArguments()[0]).addAllocation(new Throwable());
-            return null;
-        }).when(mTunerService).addTunable(any(), any());
-        doAnswer(invocation -> {
-            tracker.getLeakInfo(invocation.getArguments()[0]).clearAllocations();
-            return null;
-        }).when(mTunerService).removeTunable(any());
-    }
-
-    @Test
-    public void testFlags() {
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_START);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(DEFAULT), eq(0), eq(0), eq(0));
-        mTunablePadding.destroy();
-
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_TOP);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(0), eq(DEFAULT), eq(0), eq(0));
-        mTunablePadding.destroy();
-
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_END);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(0), eq(0), eq(DEFAULT), eq(0));
-        mTunablePadding.destroy();
-
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_BOTTOM);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(0), eq(0), eq(0), eq(DEFAULT));
-        mTunablePadding.destroy();
-    }
-
-    @Test
-    public void testRtl() {
-        when(mView.isLayoutRtl()).thenReturn(true);
-
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_END);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(DEFAULT), eq(0), eq(0), eq(0));
-        mTunablePadding.destroy();
-
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_START);
-        mTunablePadding.onTuningChanged(null, null);
-        verify(mView).setPadding(eq(0), eq(0), eq(DEFAULT), eq(0));
-        mTunablePadding.destroy();
-    }
-
-    @Test
-    public void testTuning() {
-        int value = 3;
-        mTunablePadding = TunablePadding.addTunablePadding(mView, KEY, DEFAULT,
-                TunablePadding.FLAG_START);
-        mTunablePadding.onTuningChanged(KEY, String.valueOf(value));
-
-        DisplayMetrics metrics = new DisplayMetrics();
-        mContext.getSystemService(WindowManager.class).getDefaultDisplay().getMetrics(metrics);
-        int output = (int) (metrics.density * value);
-        verify(mView).setPadding(eq(output), eq(0), eq(0), eq(0));
-
-        mTunablePadding.destroy();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index e3e20c8..5f7420d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -31,9 +31,11 @@
 import com.android.systemui.settings.UserTracker
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
 import org.junit.Before
@@ -65,20 +67,21 @@
     }
 
     @Test
-    fun registerContentObserverForUser_inputString_success() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(false),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputString_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputString_success() =
@@ -98,13 +101,14 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputString_success() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputString_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -113,24 +117,24 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserverForUser_inputString_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(true),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputString_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputString_notifyForDescendants_true() =
@@ -153,14 +157,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputString_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -169,23 +174,23 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserverForUser_inputUri_success() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING_URI,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(false),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputUri_success() =
@@ -205,13 +210,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputUri_success() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING_URI,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
+
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -220,24 +227,41 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun registerContentObserverForUserAsync_callbackAfterRegister() =
+        testScope.runTest {
+            var callbackCalled = false
+            val runnable = { callbackCalled = true }
+
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                mContentObserver,
+                mUserTracker.userId,
+                runnable
+            )
+            testScope.advanceUntilIdle()
+            assertThat(callbackCalled).isTrue()
+        }
 
     @Test
-    fun registerContentObserverForUser_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserSync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(
-                eq(TEST_SETTING_URI),
-                eq(true),
-                eq(mContentObserver),
-                eq(MAIN_USER_ID)
+    fun registerContentObserverForUser_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserSync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
             )
-    }
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(MAIN_USER_ID)
+                )
+        }
 
     @Test
     fun registerContentObserverForUserSuspend_inputUri_notifyForDescendants_true() =
@@ -260,14 +284,15 @@
         }
 
     @Test
-    fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverForUserAsync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver,
-            mUserTracker.userId
-        )
-        testScope.launch {
+    fun registerContentObserverForUserAsync_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverForUserAsync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver,
+                mUserTracker.userId
+            )
+            testScope.advanceUntilIdle()
             verify(mSettings.getContentResolver())
                 .registerContentObserver(
                     eq(TEST_SETTING_URI),
@@ -276,14 +301,19 @@
                     eq(MAIN_USER_ID)
                 )
         }
-    }
 
     @Test
-    fun registerContentObserver_inputUri_success() {
-        mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0))
-    }
+    fun registerContentObserver_inputUri_success() =
+        testScope.runTest {
+            mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(false),
+                    eq(mContentObserver),
+                    eq(0)
+                )
+        }
 
     @Test
     fun registerContentObserverSuspend_inputUri_success() =
@@ -313,15 +343,21 @@
     }
 
     @Test
-    fun registerContentObserver_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverSync(
-            TEST_SETTING_URI,
-            notifyForDescendants = true,
-            mContentObserver
-        )
-        verify(mSettings.getContentResolver())
-            .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0))
-    }
+    fun registerContentObserver_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverSync(
+                TEST_SETTING_URI,
+                notifyForDescendants = true,
+                mContentObserver
+            )
+            verify(mSettings.getContentResolver())
+                .registerContentObserver(
+                    eq(TEST_SETTING_URI),
+                    eq(true),
+                    eq(mContentObserver),
+                    eq(0)
+                )
+        }
 
     @Test
     fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() =
@@ -337,18 +373,19 @@
         }
 
     @Test
-    fun registerContentObserverAsync_inputUri_notifyForDescendants_true() {
-        mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
-        testScope.launch {
-            verify(mSettings.getContentResolver())
-                .registerContentObserver(
-                    eq(TEST_SETTING_URI),
-                    eq(false),
-                    eq(mContentObserver),
-                    eq(0)
-                )
+    fun registerContentObserverAsync_inputUri_notifyForDescendants_true() =
+        testScope.runTest {
+            mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
+            testScope.launch {
+                verify(mSettings.getContentResolver())
+                    .registerContentObserver(
+                        eq(TEST_SETTING_URI),
+                        eq(false),
+                        eq(mContentObserver),
+                        eq(0)
+                    )
+            }
         }
-    }
 
     @Test
     fun getString_keyPresent_returnValidValue() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java
index c81623e..49aedcc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/CsdWarningDialogTest.java
@@ -19,28 +19,46 @@
 import static android.media.AudioManager.CSD_WARNING_DOSE_REACHED_1X;
 import static android.media.AudioManager.CSD_WARNING_DOSE_REPEATED_5X;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
+import android.util.Pair;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.plugins.VolumeDialog;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+import java.util.Optional;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper
@@ -48,41 +66,109 @@
 
     private NotificationManager mNotificationManager;
     private AudioManager mAudioManager;
+    private BroadcastDispatcher mFakeBroadcastDispatcher;
+    private CsdWarningDialog mDialog;
+    private static final String DISMISS_CSD_NOTIFICATION =
+            "com.android.systemui.volume.DISMISS_CSD_NOTIFICATION";
 
     @Before
     public void setup() {
         mNotificationManager = mock(NotificationManager.class);
-        getContext().addMockSystemService(NotificationManager.class, mNotificationManager);
+        mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
 
         mAudioManager = mock(AudioManager.class);
-        getContext().addMockSystemService(AudioManager.class, mAudioManager);
+        mContext.addMockSystemService(AudioManager.class, mAudioManager);
+        mFakeBroadcastDispatcher = getFakeBroadcastDispatcher();
     }
 
     @Test
     public void create1XCsdDialogAndWait_sendsNotification() {
         FakeExecutor executor =  new FakeExecutor(new FakeSystemClock());
         // instantiate directly instead of via factory; we don't want executor to be @Background
-        CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REACHED_1X, mContext,
-                mAudioManager, mNotificationManager, executor, null);
+        mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REACHED_1X, mContext,
+                mAudioManager, mNotificationManager, executor, null,
+                Optional.of(ImmutableList.of(new Pair("", new Intent()))),
+                mFakeBroadcastDispatcher);
 
-        dialog.show();
+        mDialog.show();
         executor.advanceClockToLast();
         executor.runAllReady();
-        dialog.dismiss();
+        mDialog.dismiss();
 
         verify(mNotificationManager).notify(
                 eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class));
     }
 
     @Test
-    public void create5XCsdDiSalogAndWait_willSendNotification() {
+    public void create5XCsdDialogAndWait_willSendNotification() {
         FakeExecutor executor =  new FakeExecutor(new FakeSystemClock());
-        CsdWarningDialog dialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext,
-                mAudioManager, mNotificationManager, executor, null);
+        mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext,
+                mAudioManager, mNotificationManager, executor, null,
+                Optional.of(ImmutableList.of(new Pair("", new Intent()))),
+                mFakeBroadcastDispatcher);
 
-        dialog.show();
+        mDialog.show();
 
         verify(mNotificationManager).notify(
                 eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO), any(Notification.class));
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SOUNDDOSE_CUSTOMIZATION)
+    public void create1XCsdDialogWithActionsAndUndoIntent_willRegisterReceiverAndUndoVolume() {
+        FakeExecutor executor = new FakeExecutor(new FakeSystemClock());
+        Intent undoIntent = new Intent(VolumeDialog.ACTION_VOLUME_UNDO)
+                .setPackage(mContext.getPackageName());
+        mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext,
+                mAudioManager, mNotificationManager, executor, null,
+                Optional.of(ImmutableList.of(new Pair("Undo", undoIntent))),
+                mFakeBroadcastDispatcher);
+
+        when(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(25);
+        mDialog.show();
+        executor.advanceClockToLast();
+        executor.runAllReady();
+        mDialog.dismiss();
+        mDialog.mReceiverUndo.onReceive(mContext, undoIntent);
+
+        verify(mNotificationManager).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_CSD_LOWER_AUDIO),
+                any(Notification.class));
+        verify(mAudioManager).setStreamVolume(
+                eq(AudioManager.STREAM_MUSIC),
+                eq(25),
+                eq(AudioManager.FLAG_SHOW_UI));
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SOUNDDOSE_CUSTOMIZATION)
+    public void deleteNotificationIntent_willUnregisterAllReceivers() {
+        FakeExecutor executor = new FakeExecutor(new FakeSystemClock());
+        Intent undoIntent = new Intent(VolumeDialog.ACTION_VOLUME_UNDO)
+                .setPackage(mContext.getPackageName());
+        mDialog = new CsdWarningDialog(CSD_WARNING_DOSE_REPEATED_5X, mContext,
+                mAudioManager, mNotificationManager, executor, null,
+                Optional.of(ImmutableList.of(new Pair("Undo", undoIntent))),
+                mFakeBroadcastDispatcher);
+        Intent dismissIntent = new Intent(DISMISS_CSD_NOTIFICATION)
+                .setPackage(mContext.getPackageName());
+
+        mDialog.mReceiverDismissNotification.onReceive(mContext, dismissIntent);
+        mDialog.show();
+        executor.advanceClockToLast();
+        executor.runAllReady();
+        mDialog.dismiss();
+
+        List<ResolveInfo> resolveInfoListDismiss = mContext.getPackageManager()
+                .queryBroadcastReceivers(dismissIntent, PackageManager.GET_RESOLVED_FILTER);
+        assertThat(resolveInfoListDismiss).hasSize(0);
+        List<ResolveInfo> resolveInfoListUndo = mContext.getPackageManager()
+                .queryBroadcastReceivers(undoIntent, PackageManager.GET_RESOLVED_FILTER);
+        assertThat(resolveInfoListUndo).hasSize(0);
+    }
+
+    @After
+    public void tearDown() {
+        mDialog.destroy();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 6efb7d8..cdfcca6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.KeyguardManager;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -55,6 +56,7 @@
 import android.provider.Settings;
 import android.testing.TestableLooper;
 import android.util.Log;
+import android.util.Pair;
 import android.view.Gravity;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -92,6 +94,8 @@
 import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag;
 import com.android.systemui.volume.ui.navigation.VolumeNavigator;
 
+import com.google.common.collect.ImmutableList;
+
 import dagger.Lazy;
 
 import junit.framework.Assert;
@@ -107,6 +111,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.function.Predicate;
 
 @SmallTest
@@ -157,11 +162,12 @@
 
     private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
             new CsdWarningDialog.Factory() {
-        @Override
-        public CsdWarningDialog create(int warningType, Runnable onCleanup) {
-            return mCsdWarningDialog;
-        }
-    };
+                @Override
+                public CsdWarningDialog create(int warningType, Runnable onCleanup,
+                        Optional<ImmutableList<Pair<String, Intent>>> actionIntents) {
+                    return mCsdWarningDialog;
+                }
+            };
     @Mock
     private VibratorHelper mVibratorHelper;
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
index 0e4c923..796ec94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package android.hardware.display
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+val Kosmos.displayManager by Kosmos.Fixture { mock<DisplayManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 7c53639..0f8833c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui
 
 import android.app.ActivityManager
+import android.app.DreamManager
 import android.app.admin.DevicePolicyManager
 import android.app.trust.TrustManager
 import android.hardware.fingerprint.FingerprintManager
@@ -33,6 +34,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.ScreenLifecycle
@@ -94,6 +96,7 @@
     @get:Provides val demoModeController: DemoModeController = mock(),
     @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
     @get:Provides val dozeParameters: DozeParameters = mock(),
+    @get:Provides val dreamManager: DreamManager = mock(),
     @get:Provides val dumpManager: DumpManager = mock(),
     @get:Provides val fingerprintManager: FingerprintManager = mock(),
     @get:Provides val headsUpManager: HeadsUpManager = mock(),
@@ -132,6 +135,7 @@
     @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
     @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
     @get:Provides val communalInteractor: CommunalInteractor = mock(),
+    @get:Provides val communalSceneInteractor: CommunalSceneInteractor = mock(),
     @get:Provides val sceneLogger: SceneLogger = mock(),
     @get:Provides val trustManager: TrustManager = mock(),
     @get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationKosmos.kt
similarity index 70%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationKosmos.kt
index 0e4c923..8d01fcd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationKosmos.kt
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package com.android.systemui.communal.data.db
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
+import org.mockito.Mockito.mock
 
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
-
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+val Kosmos.defaultWidgetPopulation by
+    Kosmos.Fixture<DefaultWidgetPopulation> { mock(DefaultWidgetPopulation::class.java) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt
index 0e4c923..559a6ee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryKosmos.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.smartspace.data.repository
+package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
+val Kosmos.fakeCommunalSmartspaceRepository by Kosmos.Fixture { FakeCommunalSmartspaceRepository() }
 
-val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
+val Kosmos.communalSmartspaceRepository by
+    Kosmos.Fixture<CommunalSmartspaceRepository> { fakeCommunalSmartspaceRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
index 1884a32..14b1984 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalMediaRepository.kt
@@ -36,4 +36,18 @@
     fun mediaInactive() {
         _mediaModel.value = CommunalMediaModel.INACTIVE
     }
+
+    private var isListening = false
+
+    override fun startListening() {
+        isListening = true
+    }
+
+    override fun stopListening() {
+        isListening = false
+    }
+
+    fun isListening(): Boolean {
+        return isListening
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt
new file mode 100644
index 0000000..904ab4b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSmartspaceRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 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.communal.data.repository
+
+import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeCommunalSmartspaceRepository : CommunalSmartspaceRepository {
+
+    private val _timers = MutableStateFlow<List<CommunalSmartspaceTimer>>(emptyList())
+    override val timers: Flow<List<CommunalSmartspaceTimer>> = _timers
+
+    fun setTimers(timers: List<CommunalSmartspaceTimer>) {
+        _timers.value = timers
+    }
+
+    private var isListening = false
+
+    override fun startListening() {
+        isListening = true
+    }
+
+    override fun stopListening() {
+        isListening = false
+    }
+
+    fun isListening(): Boolean {
+        return isListening
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index b58861b..eb92785 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import android.os.userManager
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalSmartspaceRepository
 import com.android.systemui.communal.data.repository.communalWidgetRepository
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.flags.Flags
@@ -34,7 +35,6 @@
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.settings.userTracker
-import com.android.systemui.smartspace.data.repository.smartspaceRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.mockito.mock
 
@@ -47,7 +47,7 @@
         widgetRepository = communalWidgetRepository,
         communalPrefsInteractor = communalPrefsInteractor,
         mediaRepository = communalMediaRepository,
-        smartspaceRepository = smartspaceRepository,
+        smartspaceRepository = communalSmartspaceRepository,
         keyguardInteractor = keyguardInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         communalSettingsInteractor = communalSettingsInteractor,
@@ -64,13 +64,17 @@
 
 val Kosmos.editWidgetsActivityStarter by Fixture<EditWidgetsActivityStarter> { mock() }
 
-suspend fun Kosmos.setCommunalAvailable(available: Boolean) {
-    fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, available)
-    if (available) {
+suspend fun Kosmos.setCommunalEnabled(enabled: Boolean) {
+    fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, enabled)
+    if (enabled) {
         fakeUserRepository.asMainUser()
     } else {
         fakeUserRepository.asDefaultUser()
     }
+}
+
+suspend fun Kosmos.setCommunalAvailable(available: Boolean) {
+    setCommunalEnabled(available)
     with(fakeKeyguardRepository) {
         setIsEncryptedOrLockdown(!available)
         setKeyguardShowing(available)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index f436a68..001b55b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
 import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
 import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource
+import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.InputShortcutsSource
 import com.android.systemui.keyboard.shortcut.data.source.KeyboardShortcutGroupsSource
 import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsSource
@@ -67,18 +68,23 @@
         )
     }
 
-val Kosmos.shortcutHelperInputShortcutsSource by
+var Kosmos.shortcutHelperInputShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture { InputShortcutsSource(mainResources, windowManager) }
 
+var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by
+    Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) }
+
 val Kosmos.shortcutHelperCategoriesRepository by
     Kosmos.Fixture {
         ShortcutHelperCategoriesRepository(
             applicationContext,
+            applicationCoroutineScope,
             testDispatcher,
             shortcutHelperSystemShortcutsSource,
             shortcutHelperMultiTaskingShortcutsSource,
             shortcutHelperAppCategoriesShortcutsSource,
             shortcutHelperInputShortcutsSource,
+            shortcutHelperCurrentAppShortcutsSource,
             fakeInputManager.inputManager,
             shortcutHelperStateRepository,
         )
@@ -91,7 +97,8 @@
             applicationContext,
             broadcastDispatcher,
             fakeCommandQueue,
-            windowManager
+            fakeInputManager,
+            windowManager,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
index 40510db..3e09b23 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.Intent
+import android.hardware.input.FakeInputManager
 import android.view.KeyboardShortcutGroup
 import android.view.WindowManager
 import android.view.WindowManager.KeyboardShortcutsReceiver
@@ -31,6 +32,7 @@
     private val context: Context,
     private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
     private val fakeCommandQueue: FakeCommandQueue,
+    private val fakeInputManager: FakeInputManager,
     windowManager: WindowManager
 ) {
 
@@ -79,6 +81,7 @@
     }
 
     fun toggle(deviceId: Int) {
+        fakeInputManager.addPhysicalKeyboard(deviceId)
         fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) }
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index 446652c..126d858 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.service.dream.dreamManager
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
@@ -36,9 +38,11 @@
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             communalInteractor = communalInteractor,
+            communalSceneInteractor = communalSceneInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
             deviceEntryRepository = deviceEntryRepository,
             wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
+            dreamManager = dreamManager
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
index f253e94..81ba77a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.mediaprojection.data.repository
 
+import android.hardware.display.displayManager
 import android.os.Handler
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -30,6 +31,7 @@
     Kosmos.Fixture {
         MediaProjectionManagerRepository(
             mediaProjectionManager = fakeMediaProjectionManager.mediaProjectionManager,
+            displayManager = displayManager,
             handler = Handler.getMain(),
             applicationScope = applicationCoroutineScope,
             tasksRepository = activityTaskManagerTasksRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt
deleted file mode 100644
index 862e52d..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/FakeSmartspaceRepository.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.android.systemui.smartspace.data.repository
-
-import android.app.smartspace.SmartspaceTarget
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class FakeSmartspaceRepository(
-    smartspaceRemoteViewsEnabled: Boolean = true,
-) : SmartspaceRepository {
-
-    override val isSmartspaceRemoteViewsEnabled = smartspaceRemoteViewsEnabled
-
-    private val _communalSmartspaceTargets: MutableStateFlow<List<SmartspaceTarget>> =
-        MutableStateFlow(emptyList())
-    override val communalSmartspaceTargets: Flow<List<SmartspaceTarget>> =
-        _communalSmartspaceTargets
-
-    fun setCommunalSmartspaceTargets(targets: List<SmartspaceTarget>) {
-        _communalSmartspaceTargets.value = targets
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
index 144fe26..2335f21 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
 import android.content.applicationContext
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
@@ -34,6 +33,5 @@
             mediaRouterChipInteractor = mediaRouterChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
-            dialogTransitionAnimator = mockDialogTransitionAnimator,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
index 1d06947..2773f82 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
 
 import android.content.applicationContext
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
@@ -31,7 +30,6 @@
             context = applicationContext,
             interactor = screenRecordChipInteractor,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
-            dialogTransitionAnimator = mockDialogTransitionAnimator,
             systemClock = fakeSystemClock,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
index 2e475a3..1b3108c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
 import android.content.applicationContext
-import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
@@ -32,6 +31,5 @@
             mediaProjectionChipInteractor = mediaProjectionChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
-            dialogTransitionAnimator = mockDialogTransitionAnimator,
         )
     }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 1cd20ed..9d4310c 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -215,6 +215,7 @@
         "power_hint_flags_lib",
         "biometrics_flags_lib",
         "am_flags_lib",
+        "updates_flags_lib",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
         "com_android_wm_shell_flags_lib",
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 21947ba..95dbaae 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -1016,6 +1016,7 @@
             // Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the
             // kernel log
             doSysRq('w');
+            doSysRq('m');
             doSysRq('l');
         }
 
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index cc476a3..69ee8fc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2076,7 +2076,8 @@
                 app.setPersistent(true);
                 app.setPid(MY_PID);
                 app.mState.setMaxAdj(ProcessList.SYSTEM_ADJ);
-                app.makeActive(mSystemThread.getApplicationThread(), mProcessStats);
+                app.makeActive(new ApplicationThreadDeferred(mSystemThread.getApplicationThread()),
+                        mProcessStats);
                 app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_SYSTEM);
                 addPidLocked(app);
                 updateLruProcessLocked(app, false, null);
@@ -4874,7 +4875,7 @@
             // Make app active after binding application or client may be running requests (e.g
             // starting activities) before it is ready.
             synchronized (mProcLock) {
-                app.makeActive(thread, mProcessStats);
+                app.makeActive(new ApplicationThreadDeferred(thread), mProcessStats);
                 checkTime(startTime, "attachApplicationLocked: immediately after bindApplication");
             }
             app.setPendingFinishAttach(true);
diff --git a/services/core/java/com/android/server/am/ApplicationThreadDeferred.java b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java
new file mode 100644
index 0000000..b0f9b53
--- /dev/null
+++ b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 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.am;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.annotation.IntDef;
+import android.app.IApplicationThread;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * A subclass of {@link IApplicationThread} that defers certain binder calls while the process is
+ * paused (frozen).  Any deferred calls are executed when the process is unpaused.  In some cases,
+ * multiple instances of deferred calls are collapsed into a single call when the process is
+ * unpaused.
+ *
+ * {@hide}
+ */
+class ApplicationThreadDeferred extends ApplicationThreadFilter {
+
+    static final String TAG = TAG_WITH_CLASS_NAME ? "ApplicationThreadDeferred" : TAG_AM;
+
+    // The flag that enables the deferral behavior of this class.  If the flag is disabled then
+    // the class behaves exactly like an ApplicationThreadFilter.
+    private static boolean deferBindersWhenPaused() {
+        return Flags.deferBindersWhenPaused();
+    }
+
+    // The list of notifications that may be deferred.
+    private static final int CLEAR_DNS_CACHE = 0;
+    private static final int UPDATE_TIME_ZONE = 1;
+    private static final int SCHEDULE_LOW_MEMORY = 2;
+    private static final int UPDATE_HTTP_PROXY = 3;
+    private static final int NOTIFICATION_COUNT = 4;
+
+    @IntDef(value = {
+                CLEAR_DNS_CACHE,
+                UPDATE_TIME_ZONE,
+                SCHEDULE_LOW_MEMORY,
+                UPDATE_HTTP_PROXY
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface NotificationType {};
+
+    private final Object mLock = new Object();
+
+    // If this is true, notifications should be queued for later delivery.  If this is false,
+    // notifications should be delivered immediately.
+    @GuardedBy("mLock")
+    private boolean mPaused = false;
+
+    // An operation is a lambda that throws an exception.
+    private interface Operation {
+        void run() throws RemoteException;
+    }
+
+    // The array of operations.
+    @GuardedBy("mLock")
+    private final Operation[] mOperations = new Operation[NOTIFICATION_COUNT];
+
+    // The array of operations that actually pending right now.
+    @GuardedBy("mLock")
+    private final boolean[] mPending = new boolean[NOTIFICATION_COUNT];
+
+    // When true, binder calls to paused processes will be deferred until the process is unpaused.
+    private final boolean mDefer;
+
+    /** Create an instance with a base thread and a deferral enable flag. */
+    @VisibleForTesting
+    public ApplicationThreadDeferred(IApplicationThread thread, boolean defer) {
+        super(thread);
+
+        mDefer = defer;
+
+        mOperations[CLEAR_DNS_CACHE] = () -> { super.clearDnsCache(); };
+        mOperations[UPDATE_TIME_ZONE] = () -> { super.updateTimeZone(); };
+        mOperations[SCHEDULE_LOW_MEMORY] = () -> { super.scheduleLowMemory(); };
+        mOperations[UPDATE_HTTP_PROXY] = () -> { super.updateHttpProxy(); };
+    }
+
+    /** Create an instance with a base flag, using the system deferral enable flag. */
+    public ApplicationThreadDeferred(IApplicationThread thread) {
+        this(thread, deferBindersWhenPaused());
+    }
+
+    /** The process is being paused.  Start deferring calls. */
+    void onProcessPaused() {
+        synchronized (mLock) {
+            mPaused = true;
+        }
+    }
+
+    /** The process is no longer paused.  Drain any deferred calls. */
+    void onProcessUnpaused() {
+        synchronized (mLock) {
+            mPaused = false;
+            try {
+                for (int i = 0; i < mOperations.length; i++) {
+                    if (mPending[i]) {
+                        mOperations[i].run();
+                    }
+                }
+            } catch (RemoteException e) {
+                // Swallow the exception.  The caller is not expecting it.  Remote exceptions
+                // happen if a has process died; there is no need to report it here.
+            } finally {
+                Arrays.fill(mPending, false);
+            }
+        }
+    }
+
+    /** The pause operation has been canceled.  Drain any deferred calls. */
+    void onProcessPausedCancelled() {
+        onProcessUnpaused();
+    }
+
+    /**
+     * If the thread is not paused, execute the operation.  Otherwise, save it to the pending
+     * list.
+     */
+    private void execute(@NotificationType int tag) throws RemoteException {
+        synchronized (mLock) {
+            if (mPaused && mDefer) {
+                mPending[tag] = true;
+                return;
+            }
+        }
+        // Outside the synchronization block to avoid contention.
+        mOperations[tag].run();
+    }
+
+    @Override
+    public void clearDnsCache() throws RemoteException {
+        execute(CLEAR_DNS_CACHE);
+    }
+
+    @Override
+    public void updateTimeZone() throws RemoteException {
+        execute(UPDATE_TIME_ZONE);
+    }
+
+    @Override
+    public void scheduleLowMemory() throws RemoteException {
+        execute(SCHEDULE_LOW_MEMORY);
+    }
+
+    @Override
+    public void updateHttpProxy() throws RemoteException {
+        execute(UPDATE_HTTP_PROXY);
+    }
+}
diff --git a/services/core/java/com/android/server/am/ApplicationThreadFilter.java b/services/core/java/com/android/server/am/ApplicationThreadFilter.java
new file mode 100644
index 0000000..d049305
--- /dev/null
+++ b/services/core/java/com/android/server/am/ApplicationThreadFilter.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright (C) 2024 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.am;
+
+
+class ApplicationThreadFilter implements android.app.IApplicationThread {
+    private final android.app.IApplicationThread mBase;
+    public ApplicationThreadFilter(android.app.IApplicationThread base) { mBase = base; }
+    android.app.IApplicationThread getBase() { return mBase; }
+    public android.os.IBinder asBinder() {
+        return mBase.asBinder();
+    }
+
+    @Override
+    public void scheduleReceiver(android.content.Intent intent,
+            android.content.pm.ActivityInfo info,
+            android.content.res.CompatibilityInfo compatInfo,
+            int resultCode,
+            String data,
+            android.os.Bundle extras,
+            boolean ordered,
+            boolean assumeDelivered,
+            int sendingUser,
+            int processState,
+            int sentFromUid,
+            String sentFromPackage)
+            throws android.os.RemoteException {
+        mBase.scheduleReceiver(intent,
+                info,
+                compatInfo,
+                resultCode,
+                data,
+                extras,
+                ordered,
+                assumeDelivered,
+                sendingUser,
+                processState,
+                sentFromUid,
+                sentFromPackage);
+    }
+    @Override
+    public void scheduleReceiverList(java.util.List<android.app.ReceiverInfo> info)
+            throws android.os.RemoteException {
+        mBase.scheduleReceiverList(info);
+    }
+    @Override
+    public void scheduleCreateService(android.os.IBinder token,
+            android.content.pm.ServiceInfo info,
+            android.content.res.CompatibilityInfo compatInfo,
+            int processState)
+            throws android.os.RemoteException {
+        mBase.scheduleCreateService(token,
+                info,
+                compatInfo,
+                processState);
+    }
+    @Override
+    public void scheduleStopService(android.os.IBinder token)
+            throws android.os.RemoteException {
+        mBase.scheduleStopService(token);
+    }
+    @Override
+    public void bindApplication(String packageName,
+            android.content.pm.ApplicationInfo info,
+            String sdkSandboxClientAppVolumeUuid,
+            String sdkSandboxClientAppPackage,
+            boolean isSdkInSandbox,
+            android.content.pm.ProviderInfoList providerList,
+            android.content.ComponentName testName,
+            android.app.ProfilerInfo profilerInfo,
+            android.os.Bundle testArguments,
+            android.app.IInstrumentationWatcher testWatcher,
+            android.app.IUiAutomationConnection uiAutomationConnection,
+            int debugMode,
+            boolean enableBinderTracking,
+            boolean trackAllocation,
+            boolean restrictedBackupMode,
+            boolean persistent,
+            android.content.res.Configuration config,
+            android.content.res.CompatibilityInfo compatInfo,
+            java.util.Map services,
+            android.os.Bundle coreSettings,
+            String buildSerial,
+            android.content.AutofillOptions autofillOptions,
+            android.content.ContentCaptureOptions contentCaptureOptions,
+            long[] disabledCompatChanges,
+            long[] loggableCompatChanges,
+            android.os.SharedMemory serializedSystemFontMap,
+            long startRequestedElapsedTime,
+            long startRequestedUptime)
+            throws android.os.RemoteException {
+        mBase.bindApplication(packageName,
+                info,
+                sdkSandboxClientAppVolumeUuid,
+                sdkSandboxClientAppPackage,
+                isSdkInSandbox,
+                providerList,
+                testName,
+                profilerInfo,
+                testArguments,
+                testWatcher,
+                uiAutomationConnection,
+                debugMode,
+                enableBinderTracking,
+                trackAllocation,
+                restrictedBackupMode,
+                persistent,
+                config,
+                compatInfo,
+                services,
+                coreSettings,
+                buildSerial,
+                autofillOptions,
+                contentCaptureOptions,
+                disabledCompatChanges,
+                loggableCompatChanges,
+                serializedSystemFontMap,
+                startRequestedElapsedTime,
+                startRequestedUptime);
+    }
+    @Override
+    public void runIsolatedEntryPoint(String entryPoint,
+            String[] entryPointArgs)
+            throws android.os.RemoteException {
+        mBase.runIsolatedEntryPoint(entryPoint,
+                entryPointArgs);
+    }
+    @Override
+    public void scheduleExit()
+            throws android.os.RemoteException {
+        mBase.scheduleExit();
+    }
+    @Override
+    public void scheduleServiceArgs(android.os.IBinder token,
+            android.content.pm.ParceledListSlice args)
+            throws android.os.RemoteException {
+        mBase.scheduleServiceArgs(token,
+                args);
+    }
+    @Override
+    public void updateTimeZone()
+            throws android.os.RemoteException {
+        mBase.updateTimeZone();
+    }
+    @Override
+    public void processInBackground()
+            throws android.os.RemoteException {
+        mBase.processInBackground();
+    }
+    @Override
+    public void scheduleBindService(android.os.IBinder token,
+            android.content.Intent intent,
+            boolean rebind,
+            int processState,
+            long bindSeq)
+            throws android.os.RemoteException {
+        mBase.scheduleBindService(token,
+                intent,
+                rebind,
+                processState,
+                bindSeq);
+    }
+    @Override
+    public void scheduleUnbindService(android.os.IBinder token,
+            android.content.Intent intent)
+            throws android.os.RemoteException {
+        mBase.scheduleUnbindService(token,
+                intent);
+    }
+    @Override
+    public void dumpService(android.os.ParcelFileDescriptor fd,
+            android.os.IBinder servicetoken,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpService(fd,
+                servicetoken,
+                args);
+    }
+    @Override
+    public void scheduleRegisteredReceiver(android.content.IIntentReceiver receiver,
+            android.content.Intent intent,
+            int resultCode,
+            String data,
+            android.os.Bundle extras,
+            boolean ordered,
+            boolean sticky,
+            boolean assumeDelivered,
+            int sendingUser,
+            int processState,
+            int sentFromUid,
+            String sentFromPackage)
+            throws android.os.RemoteException {
+        mBase.scheduleRegisteredReceiver(receiver,
+                intent,
+                resultCode,
+                data,
+                extras,
+                ordered,
+                sticky,
+                assumeDelivered,
+                sendingUser,
+                processState,
+                sentFromUid,
+                sentFromPackage);
+    }
+    @Override
+    public void scheduleLowMemory()
+            throws android.os.RemoteException {
+        mBase.scheduleLowMemory();
+    }
+    @Override
+    public void profilerControl(boolean start,
+            android.app.ProfilerInfo profilerInfo,
+            int profileType)
+            throws android.os.RemoteException {
+        mBase.profilerControl(start,
+                profilerInfo,
+                profileType);
+    }
+    @Override
+    public void setSchedulingGroup(int group)
+            throws android.os.RemoteException {
+        mBase.setSchedulingGroup(group);
+    }
+    @Override
+    public void scheduleCreateBackupAgent(android.content.pm.ApplicationInfo app,
+            int backupMode,
+            int userId,
+            int operationType)
+            throws android.os.RemoteException {
+        mBase.scheduleCreateBackupAgent(app,
+                backupMode,
+                userId,
+                operationType);
+    }
+    @Override
+    public void scheduleDestroyBackupAgent(android.content.pm.ApplicationInfo app,
+            int userId)
+            throws android.os.RemoteException {
+        mBase.scheduleDestroyBackupAgent(app,
+                userId);
+    }
+    @Override
+    public void scheduleOnNewSceneTransitionInfo(android.os.IBinder token,
+            android.app.ActivityOptions.SceneTransitionInfo info)
+            throws android.os.RemoteException {
+        mBase.scheduleOnNewSceneTransitionInfo(token,
+                info);
+    }
+    @Override
+    public void scheduleSuicide()
+            throws android.os.RemoteException {
+        mBase.scheduleSuicide();
+    }
+    @Override
+    public void dispatchPackageBroadcast(int cmd,
+            String[] packages)
+            throws android.os.RemoteException {
+        mBase.dispatchPackageBroadcast(cmd,
+                packages);
+    }
+    @Override
+    public void scheduleCrash(String msg,
+            int typeId,
+            android.os.Bundle extras)
+            throws android.os.RemoteException {
+        mBase.scheduleCrash(msg,
+                typeId,
+                extras);
+    }
+    @Override
+    public void dumpHeap(boolean managed,
+            boolean mallocInfo,
+            boolean runGc,
+            String dumpBitmaps,
+            String path,
+            android.os.ParcelFileDescriptor fd,
+            android.os.RemoteCallback finishCallback)
+            throws android.os.RemoteException {
+        mBase.dumpHeap(managed,
+                mallocInfo,
+                runGc,
+                dumpBitmaps,
+                path,
+                fd,
+                finishCallback);
+    }
+    @Override
+    public void dumpActivity(android.os.ParcelFileDescriptor fd,
+            android.os.IBinder servicetoken,
+            String prefix,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpActivity(fd,
+                servicetoken,
+                prefix,
+                args);
+    }
+    @Override
+    public void dumpResources(android.os.ParcelFileDescriptor fd,
+            android.os.RemoteCallback finishCallback)
+            throws android.os.RemoteException {
+        mBase.dumpResources(fd,
+                finishCallback);
+    }
+    @Override
+    public void clearDnsCache()
+            throws android.os.RemoteException {
+        mBase.clearDnsCache();
+    }
+    @Override
+    public void updateHttpProxy()
+            throws android.os.RemoteException {
+        mBase.updateHttpProxy();
+    }
+    @Override
+    public void setCoreSettings(android.os.Bundle coreSettings)
+            throws android.os.RemoteException {
+        mBase.setCoreSettings(coreSettings);
+    }
+    @Override
+    public void updatePackageCompatibilityInfo(String pkg,
+            android.content.res.CompatibilityInfo info)
+            throws android.os.RemoteException {
+        mBase.updatePackageCompatibilityInfo(pkg,
+                info);
+    }
+    @Override
+    public void scheduleTrimMemory(int level)
+            throws android.os.RemoteException {
+        mBase.scheduleTrimMemory(level);
+    }
+    @Override
+    public void dumpMemInfo(android.os.ParcelFileDescriptor fd,
+            android.os.Debug.MemoryInfo mem,
+            boolean checkin,
+            boolean dumpInfo,
+            boolean dumpDalvik,
+            boolean dumpSummaryOnly,
+            boolean dumpUnreachable,
+            boolean dumpAllocatorLogs,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpMemInfo(fd,
+                mem,
+                checkin,
+                dumpInfo,
+                dumpDalvik,
+                dumpSummaryOnly,
+                dumpUnreachable,
+                dumpAllocatorLogs,
+                args);
+    }
+    @Override
+    public void dumpMemInfoProto(android.os.ParcelFileDescriptor fd,
+            android.os.Debug.MemoryInfo mem,
+            boolean dumpInfo,
+            boolean dumpDalvik,
+            boolean dumpSummaryOnly,
+            boolean dumpUnreachable,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpMemInfoProto(fd,
+                mem,
+                dumpInfo,
+                dumpDalvik,
+                dumpSummaryOnly,
+                dumpUnreachable,
+                args);
+    }
+    @Override
+    public void dumpGfxInfo(android.os.ParcelFileDescriptor fd,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpGfxInfo(fd,
+                args);
+    }
+    @Override
+    public void dumpCacheInfo(android.os.ParcelFileDescriptor fd,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpCacheInfo(fd,
+                args);
+    }
+    @Override
+    public void dumpProvider(android.os.ParcelFileDescriptor fd,
+            android.os.IBinder servicetoken,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpProvider(fd,
+                servicetoken,
+                args);
+    }
+    @Override
+    public void dumpDbInfo(android.os.ParcelFileDescriptor fd,
+            String[] args)
+            throws android.os.RemoteException {
+        mBase.dumpDbInfo(fd,
+                args);
+    }
+    @Override
+    public void unstableProviderDied(android.os.IBinder provider)
+            throws android.os.RemoteException {
+        mBase.unstableProviderDied(provider);
+    }
+    @Override
+    public void requestAssistContextExtras(android.os.IBinder activityToken,
+            android.os.IBinder requestToken,
+            int requestType,
+            int sessionId,
+            int flags)
+            throws android.os.RemoteException {
+        mBase.requestAssistContextExtras(activityToken,
+                requestToken,
+                requestType,
+                sessionId,
+                flags);
+    }
+    @Override
+    public void scheduleTranslucentConversionComplete(android.os.IBinder token,
+            boolean timeout)
+            throws android.os.RemoteException {
+        mBase.scheduleTranslucentConversionComplete(token,
+                timeout);
+    }
+    @Override
+    public void setProcessState(int state)
+            throws android.os.RemoteException {
+        mBase.setProcessState(state);
+    }
+    @Override
+    public void scheduleInstallProvider(android.content.pm.ProviderInfo provider)
+            throws android.os.RemoteException {
+        mBase.scheduleInstallProvider(provider);
+    }
+    @Override
+    public void updateTimePrefs(int timeFormatPreference)
+            throws android.os.RemoteException {
+        mBase.updateTimePrefs(timeFormatPreference);
+    }
+    @Override
+    public void scheduleEnterAnimationComplete(android.os.IBinder token)
+            throws android.os.RemoteException {
+        mBase.scheduleEnterAnimationComplete(token);
+    }
+    @Override
+    public void notifyCleartextNetwork(byte[] firstPacket)
+            throws android.os.RemoteException {
+        mBase.notifyCleartextNetwork(firstPacket);
+    }
+    @Override
+    public void startBinderTracking()
+            throws android.os.RemoteException {
+        mBase.startBinderTracking();
+    }
+    @Override
+    public void stopBinderTrackingAndDump(android.os.ParcelFileDescriptor fd)
+            throws android.os.RemoteException {
+        mBase.stopBinderTrackingAndDump(fd);
+    }
+    @Override
+    public void scheduleLocalVoiceInteractionStarted(android.os.IBinder token,
+            com.android.internal.app.IVoiceInteractor voiceInteractor)
+            throws android.os.RemoteException {
+        mBase.scheduleLocalVoiceInteractionStarted(token,
+                voiceInteractor);
+    }
+    @Override
+    public void handleTrustStorageUpdate()
+            throws android.os.RemoteException {
+        mBase.handleTrustStorageUpdate();
+    }
+    @Override
+    public void attachAgent(String path)
+            throws android.os.RemoteException {
+        mBase.attachAgent(path);
+    }
+    @Override
+    public void attachStartupAgents(String dataDir)
+            throws android.os.RemoteException {
+        mBase.attachStartupAgents(dataDir);
+    }
+    @Override
+    public void scheduleApplicationInfoChanged(android.content.pm.ApplicationInfo ai)
+            throws android.os.RemoteException {
+        mBase.scheduleApplicationInfoChanged(ai);
+    }
+    @Override
+    public void setNetworkBlockSeq(long procStateSeq)
+            throws android.os.RemoteException {
+        mBase.setNetworkBlockSeq(procStateSeq);
+    }
+    @Override
+    public void scheduleTransaction(android.app.servertransaction.ClientTransaction transaction)
+            throws android.os.RemoteException {
+        mBase.scheduleTransaction(transaction);
+    }
+    @Override
+    public void scheduleTaskFragmentTransaction(android.window.ITaskFragmentOrganizer organizer,
+            android.window.TaskFragmentTransaction transaction)
+            throws android.os.RemoteException {
+        mBase.scheduleTaskFragmentTransaction(organizer,
+                transaction);
+    }
+    @Override
+    public void requestDirectActions(android.os.IBinder activityToken,
+            com.android.internal.app.IVoiceInteractor intractor,
+            android.os.RemoteCallback cancellationCallback,
+            android.os.RemoteCallback callback)
+            throws android.os.RemoteException {
+        mBase.requestDirectActions(activityToken,
+                intractor,
+                cancellationCallback,
+                callback);
+    }
+    @Override
+    public void performDirectAction(android.os.IBinder activityToken,
+            String actionId,
+            android.os.Bundle arguments,
+            android.os.RemoteCallback cancellationCallback,
+            android.os.RemoteCallback resultCallback)
+            throws android.os.RemoteException {
+        mBase.performDirectAction(activityToken,
+                actionId,
+                arguments,
+                cancellationCallback,
+                resultCallback);
+    }
+    @Override
+    public void notifyContentProviderPublishStatus(android.app.ContentProviderHolder holder,
+            String authorities,
+            int userId,
+            boolean published)
+            throws android.os.RemoteException {
+        mBase.notifyContentProviderPublishStatus(holder,
+                authorities,
+                userId,
+                published);
+    }
+    @Override
+    public void instrumentWithoutRestart(android.content.ComponentName instrumentationName,
+            android.os.Bundle instrumentationArgs,
+            android.app.IInstrumentationWatcher instrumentationWatcher,
+            android.app.IUiAutomationConnection instrumentationUiConnection,
+            android.content.pm.ApplicationInfo targetInfo)
+            throws android.os.RemoteException {
+        mBase.instrumentWithoutRestart(instrumentationName,
+                instrumentationArgs,
+                instrumentationWatcher,
+                instrumentationUiConnection,
+                targetInfo);
+    }
+    @Override
+    public void updateUiTranslationState(android.os.IBinder activityToken,
+            int state,
+            android.view.translation.TranslationSpec sourceSpec,
+            android.view.translation.TranslationSpec targetSpec,
+            java.util.List<android.view.autofill.AutofillId> viewIds,
+            android.view.translation.UiTranslationSpec uiTranslationSpec)
+            throws android.os.RemoteException {
+        mBase.updateUiTranslationState(activityToken,
+                state,
+                sourceSpec,
+                targetSpec,
+                viewIds,
+                uiTranslationSpec);
+    }
+    @Override
+    public void scheduleTimeoutService(android.os.IBinder token,
+            int startId)
+            throws android.os.RemoteException {
+        mBase.scheduleTimeoutService(token,
+                startId);
+    }
+    @Override
+    public void scheduleTimeoutServiceForType(android.os.IBinder token,
+            int startId,
+            int fgsType)
+            throws android.os.RemoteException {
+        mBase.scheduleTimeoutServiceForType(token,
+                startId,
+                fgsType);
+    }
+    @Override
+    public void schedulePing(android.os.RemoteCallback pong)
+            throws android.os.RemoteException {
+        mBase.schedulePing(pong);
+    }
+}
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 6433f2c..1c4ffbb 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -2656,6 +2656,7 @@
         // PIDs that run out of async binder buffer when being frozen
         ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>();
 
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "binderErrorSync");
         for (int i = 0; i < pids.size(); i++) {
             int current = pids.get(i);
             try {
@@ -2684,6 +2685,7 @@
                 Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current);
             }
         }
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
 
         // TODO: when kernel binder driver supports, poll the binder status directly.
         // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the
@@ -2693,6 +2695,8 @@
         if (pidsAsync == null || pidsAsync.size() == 0) {
             return;
         }
+
+        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "binderErrorAsync");
         new BinderfsStatsReader().handleFreeAsyncSpace(
                 // Check if the frozen process has pending async calls
                 pidsAsync::contains,
@@ -2710,5 +2714,6 @@
 
                 // Log the error if binderfs stats can't be accesses or correctly parsed
                 exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats"));
+        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
     }
 }
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index a74c489..3e71d00 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -151,7 +151,7 @@
      * (in which case we are in the process of launching the app).
      */
     @CompositeRWLock({"mService", "mProcLock"})
-    private IApplicationThread mThread;
+    private ApplicationThreadDeferred mThread;
 
     /**
      * Instance of {@link #mThread} that will always meet the {@code oneway}
@@ -737,15 +737,15 @@
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    public void makeActive(IApplicationThread thread, ProcessStatsService tracker) {
+    public void makeActive(ApplicationThreadDeferred thread, ProcessStatsService tracker) {
         mProfile.onProcessActive(thread, tracker);
         mThread = thread;
         if (mPid == Process.myPid()) {
-            mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler());
+            mOnewayThread = new SameProcessApplicationThread(mThread, FgThread.getHandler());
         } else {
-            mOnewayThread = thread;
+            mOnewayThread = mThread;
         }
-        mWindowProcessController.setThread(thread);
+        mWindowProcessController.setThread(mThread);
         if (mWindowProcessController.useFifoUiScheduling()) {
             mService.mSpecifiedFifoProcesses.add(this);
         }
@@ -1436,14 +1436,17 @@
 
     void onProcessFrozen() {
         mProfile.onProcessFrozen();
+        if (mThread != null) mThread.onProcessPaused();
     }
 
     void onProcessUnfrozen() {
+        if (mThread != null) mThread.onProcessUnpaused();
         mProfile.onProcessUnfrozen();
         mServices.onProcessUnfrozen();
     }
 
     void onProcessFrozenCancelled() {
+        if (mThread != null) mThread.onProcessPausedCancelled();
         mServices.onProcessFrozenCancelled();
     }
 
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 9b380ff..5315167 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -163,7 +163,7 @@
 
 flag {
     name: "collect_logcat_on_run_synchronously"
-    namespace: "dropbox"
+    namespace: "stability"
     description: "Allow logcat collection on synchronous dropbox collection"
     bug: "324222683"
     is_fixed_read_only: true
@@ -171,8 +171,16 @@
 
 flag {
     name: "enable_dropbox_watchdog_headers"
-    namespace: "dropbox"
+    namespace: "stability"
     description: "Add watchdog-specific dropbox headers"
     bug: "330682397"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "defer_binders_when_paused"
+    namespace: "system_performance"
+    is_fixed_read_only: true
+    description: "Defer submitting binder calls to paused processes."
+    bug: "327038797"
+}
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 9b37418..515e704 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -22,6 +22,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.brightness.clamper.HdrClamper;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.feature.DisplayManagerFlags;
 
 import java.io.PrintWriter;
@@ -157,7 +158,7 @@
     private void updateHdrClamper(DisplayDeviceInfo info, IBinder token,
             DisplayDeviceConfig displayDeviceConfig) {
         if (mUseHdrClamper) {
-            DisplayDeviceConfig.HighBrightnessModeData hbmData =
+            HighBrightnessModeData hbmData =
                     displayDeviceConfig.getHighBrightnessModeData();
             float minimumHdrPercentOfScreen =
                     hbmData == null ? -1f : hbmData.minimumHdrPercentOfScreen;
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index e4db634..f5231ae 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -53,9 +53,9 @@
 import com.android.server.display.config.DisplayConfiguration;
 import com.android.server.display.config.DisplayQuirks;
 import com.android.server.display.config.EvenDimmerBrightnessData;
-import com.android.server.display.config.HbmTiming;
 import com.android.server.display.config.HdrBrightnessData;
 import com.android.server.display.config.HighBrightnessMode;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.config.HysteresisLevels;
 import com.android.server.display.config.IdleScreenRefreshRateTimeout;
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
@@ -75,8 +75,6 @@
 import com.android.server.display.config.RefreshRateThrottlingMap;
 import com.android.server.display.config.RefreshRateThrottlingPoint;
 import com.android.server.display.config.RefreshRateZone;
-import com.android.server.display.config.SdrHdrRatioMap;
-import com.android.server.display.config.SdrHdrRatioPoint;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.config.ThermalStatus;
 import com.android.server.display.config.ThermalThrottling;
@@ -302,6 +300,19 @@
  *         <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis>
  *         <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis>
  *         <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis>
+ *         <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm>
+ *         <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm>
+ *         <allowInLowPowerMode>true</allowInLowPowerMode>
+ *         <sdrHdrRatioMap>
+ *             <point>
+ *                 <first>2.0</first>
+ *                 <second>4.0</second>
+ *             </point>
+ *             <point>
+ *                 <first>100</first>
+ *                 <second>8.0</second>
+ *             </point>
+ *         </sdrHdrRatioMap>
  *      </hdrBrightnessConfig>
  *      <luxThrottling>
  *        <brightnessLimitMap>
@@ -659,9 +670,6 @@
     // Invalid value of AutoBrightness brightening and darkening light debounce
     private static final int INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE = -1;
 
-    @VisibleForTesting
-    static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f;
-
     private final Context mContext;
 
     // The details of the ambient light sensor associated with this display.
@@ -743,13 +751,13 @@
     private Spline mNitsToBacklightSpline;
 
     private List<String> mQuirks;
-    private boolean mIsHighBrightnessModeEnabled = false;
+    @Nullable
     private HighBrightnessModeData mHbmData;
     @Nullable
     private PowerThrottlingConfigData mPowerThrottlingConfigData;
     private DensityMapping mDensityMapping;
     private String mLoadedFrom = null;
-    private Spline mSdrToHdrRatioSpline;
+
 
     // Represents the auto-brightness brightening light debounce.
     private long mAutoBrightnessBrighteningLightDebounce =
@@ -872,7 +880,7 @@
     private final DisplayManagerFlags mFlags;
 
     @VisibleForTesting
-    DisplayDeviceConfig(Context context, DisplayManagerFlags flags) {
+    public DisplayDeviceConfig(Context context, DisplayManagerFlags flags) {
         mContext = context;
         mFlags = flags;
     }
@@ -1155,7 +1163,7 @@
      * @return true if there is sdrHdrRatioMap, false otherwise.
      */
     public boolean hasSdrToHdrRatioSpline() {
-        return mSdrToHdrRatioSpline != null;
+        return mHbmData != null && mHbmData.sdrToHdrRatioSpline != null;
     }
 
     /**
@@ -1165,7 +1173,8 @@
      * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists.
      */
     public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) {
-        if (mSdrToHdrRatioSpline == null) {
+        Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null;
+        if (sdrToHdrSpline == null) {
             return PowerManager.BRIGHTNESS_INVALID;
         }
 
@@ -1175,7 +1184,7 @@
             return PowerManager.BRIGHTNESS_INVALID;
         }
 
-        float ratio = Math.min(mSdrToHdrRatioSpline.interpolate(nits), maxDesiredHdrSdrRatio);
+        float ratio = Math.min(sdrToHdrSpline.interpolate(nits), maxDesiredHdrSdrRatio);
         float hdrNits = nits * ratio;
         if (getNitsToBacklightSpline() == null) {
             return PowerManager.BRIGHTNESS_INVALID;
@@ -1321,13 +1330,11 @@
      * @return high brightness mode configuration data for the display.
      */
     public HighBrightnessModeData getHighBrightnessModeData() {
-        if (!mIsHighBrightnessModeEnabled || mHbmData == null) {
+        if  (mHbmData == null || !mHbmData.isHighBrightnessModeEnabled) {
             return null;
         }
 
-        HighBrightnessModeData hbmData = new HighBrightnessModeData();
-        mHbmData.copyTo(hbmData);
-        return hbmData;
+        return mHbmData;
     }
 
     /**
@@ -1604,11 +1611,10 @@
                 + ", mBacklightMaximum=" + mBacklightMaximum
                 + ", mBrightnessDefault=" + mBrightnessDefault
                 + ", mQuirks=" + mQuirks
-                + ", mIsHighBrightnessModeEnabled=" + mIsHighBrightnessModeEnabled
                 + "\n"
                 + "mLuxThrottlingData=" + mLuxThrottlingData
                 + ", mHbmData=" + mHbmData
-                + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
+
                 + ", mThermalBrightnessThrottlingDataMapByThrottlingId="
                 + mThermalBrightnessThrottlingDataMapByThrottlingId
                 + "\n"
@@ -1715,7 +1721,7 @@
     }
 
     @VisibleForTesting
-    boolean initFromFile(File configFile) {
+    public boolean initFromFile(File configFile) {
         if (!configFile.exists()) {
             // Display configuration files aren't required to exist.
             return false;
@@ -1740,7 +1746,23 @@
                 loadBrightnessMap(config);
                 loadThermalThrottlingConfig(config);
                 loadPowerThrottlingConfigData(config);
-                loadHighBrightnessModeData(config);
+                // Backlight and evenDimmer data should be loaded for HbmData
+                mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config, (hbm) -> {
+                    float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue();
+                    if (transitionPointBacklightScale >= mBacklightMaximum) {
+                        throw new IllegalArgumentException("HBM transition point invalid. "
+                                + mHbmData.transitionPoint + " is not less than "
+                                + mBacklightMaximum);
+                    }
+                    return  getBrightnessFromBacklight(transitionPointBacklightScale);
+                });
+                if (mHbmData.isHighBrightnessModeEnabled && mHbmData.refreshRateLimit != null) {
+                    // TODO(b/331650248): cleanup, DMD can use mHbmData.refreshRateLimit
+                    mRefreshRateLimitations.add(new RefreshRateLimitation(
+                            DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE,
+                            mHbmData.refreshRateLimit));
+                }
+
                 loadLuxThrottling(config);
                 loadQuirks(config);
                 loadBrightnessRamps(config);
@@ -1938,40 +1960,6 @@
         constrainNitsAndBacklightArrays();
     }
 
-    private Spline loadSdrHdrRatioMap(HighBrightnessMode hbmConfig) {
-        final SdrHdrRatioMap sdrHdrRatioMap = hbmConfig.getSdrHdrRatioMap_all();
-
-        if (sdrHdrRatioMap == null) {
-            return null;
-        }
-
-        final List<SdrHdrRatioPoint> points = sdrHdrRatioMap.getPoint();
-        final int size = points.size();
-        if (size == 0) {
-            return null;
-        }
-
-        float[] nits = new float[size];
-        float[] ratios = new float[size];
-
-        int i = 0;
-        for (SdrHdrRatioPoint point : points) {
-            nits[i] = point.getSdrNits().floatValue();
-            if (i > 0) {
-                if (nits[i] < nits[i - 1]) {
-                    Slog.e(TAG, "sdrHdrRatioMap must be non-decreasing, ignoring rest "
-                            + " of configuration. nits: " + nits[i] + " < "
-                            + nits[i - 1]);
-                    return null;
-                }
-            }
-            ratios[i] = point.getHdrRatio().floatValue();
-            ++i;
-        }
-
-        return Spline.createSpline(nits, ratios);
-    }
-
     private void loadThermalThrottlingConfig(DisplayConfiguration config) {
         final ThermalThrottling throttlingConfig = config.getThermalThrottling();
         if (throttlingConfig == null) {
@@ -2525,49 +2513,6 @@
         }
     }
 
-    private void loadHighBrightnessModeData(DisplayConfiguration config) {
-        final HighBrightnessMode hbm = config.getHighBrightnessMode();
-        if (hbm != null) {
-            mIsHighBrightnessModeEnabled = hbm.getEnabled();
-            mHbmData = new HighBrightnessModeData();
-            mHbmData.minimumLux = hbm.getMinimumLux_all().floatValue();
-            float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue();
-            if (transitionPointBacklightScale >= mBacklightMaximum) {
-                throw new IllegalArgumentException("HBM transition point invalid. "
-                        + mHbmData.transitionPoint + " is not less than "
-                        + mBacklightMaximum);
-            }
-            mHbmData.transitionPoint =
-                    getBrightnessFromBacklight(transitionPointBacklightScale);
-            final HbmTiming hbmTiming = hbm.getTiming_all();
-            mHbmData.timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000;
-            mHbmData.timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000;
-            mHbmData.timeMinMillis = hbmTiming.getTimeMinSecs_all().longValue() * 1000;
-            mHbmData.allowInLowPowerMode = hbm.getAllowInLowPowerMode_all();
-            final RefreshRateRange rr = hbm.getRefreshRate_all();
-            if (rr != null) {
-                final float min = rr.getMinimum().floatValue();
-                final float max = rr.getMaximum().floatValue();
-                mRefreshRateLimitations.add(new RefreshRateLimitation(
-                        DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE, min, max));
-            }
-            BigDecimal minHdrPctOfScreen = hbm.getMinimumHdrPercentOfScreen_all();
-            if (minHdrPctOfScreen != null) {
-                mHbmData.minimumHdrPercentOfScreen = minHdrPctOfScreen.floatValue();
-                if (mHbmData.minimumHdrPercentOfScreen > 1
-                        || mHbmData.minimumHdrPercentOfScreen < 0) {
-                    Slog.w(TAG, "Invalid minimum HDR percent of screen: "
-                            + String.valueOf(mHbmData.minimumHdrPercentOfScreen));
-                    mHbmData.minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
-                }
-            } else {
-                mHbmData.minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
-            }
-
-            mSdrToHdrRatioSpline = loadSdrHdrRatioMap(hbm);
-        }
-    }
-
     private void loadLuxThrottling(DisplayConfiguration config) {
         LuxThrottling cfg = config.getLuxThrottling();
         if (cfg != null) {
@@ -2921,73 +2866,6 @@
     }
 
     /**
-     * Container for high brightness mode configuration data.
-     */
-    static class HighBrightnessModeData {
-        /** Minimum lux needed to enter high brightness mode */
-        public float minimumLux;
-
-        /** Brightness level at which we transition from normal to high-brightness. */
-        public float transitionPoint;
-
-        /** Whether HBM is allowed when {@code Settings.Global.LOW_POWER_MODE} is active. */
-        public boolean allowInLowPowerMode;
-
-        /** Time window for HBM. */
-        public long timeWindowMillis;
-
-        /** Maximum time HBM is allowed to be during in a {@code timeWindowMillis}. */
-        public long timeMaxMillis;
-
-        /** Minimum time that HBM can be on before being enabled. */
-        public long timeMinMillis;
-
-        /** Minimum HDR video size to enter high brightness mode */
-        public float minimumHdrPercentOfScreen;
-
-        HighBrightnessModeData() {}
-
-        HighBrightnessModeData(float minimumLux, float transitionPoint, long timeWindowMillis,
-                long timeMaxMillis, long timeMinMillis, boolean allowInLowPowerMode,
-                float minimumHdrPercentOfScreen) {
-            this.minimumLux = minimumLux;
-            this.transitionPoint = transitionPoint;
-            this.timeWindowMillis = timeWindowMillis;
-            this.timeMaxMillis = timeMaxMillis;
-            this.timeMinMillis = timeMinMillis;
-            this.allowInLowPowerMode = allowInLowPowerMode;
-            this.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen;
-        }
-
-        /**
-         * Copies the HBM data to the specified parameter instance.
-         * @param other the instance to copy data to.
-         */
-        public void copyTo(@NonNull HighBrightnessModeData other) {
-            other.minimumLux = minimumLux;
-            other.timeWindowMillis = timeWindowMillis;
-            other.timeMaxMillis = timeMaxMillis;
-            other.timeMinMillis = timeMinMillis;
-            other.transitionPoint = transitionPoint;
-            other.allowInLowPowerMode = allowInLowPowerMode;
-            other.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen;
-        }
-
-        @Override
-        public String toString() {
-            return "HBM{"
-                    + "minLux: " + minimumLux
-                    + ", transition: " + transitionPoint
-                    + ", timeWindow: " + timeWindowMillis + "ms"
-                    + ", timeMax: " + timeMaxMillis + "ms"
-                    + ", timeMin: " + timeMinMillis + "ms"
-                    + ", allowInLowPowerMode: " + allowInLowPowerMode
-                    + ", minimumHdrPercentOfScreen: " + minimumHdrPercentOfScreen
-                    + "} ";
-        }
-    }
-
-    /**
      * Container for Power throttling configuration data.
      * TODO(b/302814899): extract to separate class.
      */
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3493381b..2f3584c 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -5240,7 +5240,7 @@
                 mHandler.sendMessage(msg);
 
                 mLogicalDisplayMapper
-                        .setDeviceStateLocked(deviceState.getIdentifier());
+                        .setDeviceStateLocked(deviceState);
             }
         }
     };
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 76a561b..58309c2 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -87,6 +87,7 @@
 import com.android.server.display.brightness.strategy.DisplayBrightnessStrategyConstants;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
 import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.config.HysteresisLevels;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
@@ -2017,7 +2018,7 @@
         final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
         final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
         final String displayUniqueId = mDisplayDevice.getUniqueId();
-        final DisplayDeviceConfig.HighBrightnessModeData hbmData =
+        final HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
         final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
         return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
@@ -3251,7 +3252,7 @@
 
         HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
                 int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                float brightnessMax, HighBrightnessModeData hbmData,
                 HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
                 Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
                 Context context) {
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 47176fe..da9eef2 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -36,8 +36,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.FrameworkStatsLog;
-import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
 import com.android.server.display.DisplayManagerService.Clock;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 4791cd1..d3b41b8 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -16,12 +16,17 @@
 
 package com.android.server.display;
 
+import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP;
+import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceState;
+import android.hardware.devicestate.feature.flags.FeatureFlags;
+import android.hardware.devicestate.feature.flags.FeatureFlagsImpl;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -198,14 +203,14 @@
     private final DisplayIdProducer mIdProducer = (isDefault) ->
             isDefault ? DEFAULT_DISPLAY : sNextNonDefaultDisplayId++;
     private Layout mCurrentLayout = null;
-    private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
-    private int mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
-    private int mDeviceStateToBeAppliedAfterBoot =
-            DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+    private DeviceState mDeviceState = INVALID_DEVICE_STATE;
+    private DeviceState mPendingDeviceState = INVALID_DEVICE_STATE;
+    private DeviceState mDeviceStateToBeAppliedAfterBoot = INVALID_DEVICE_STATE;
     private boolean mBootCompleted = false;
     private boolean mInteractive;
     private final DisplayManagerFlags mFlags;
     private final SyntheticModeManager mSyntheticModeManager;
+    private final FeatureFlags mDeviceStateManagerFlags;
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
             FoldGracePeriodProvider foldGracePeriodProvider,
@@ -245,6 +250,7 @@
         mDeviceStateToLayoutMap = deviceStateToLayoutMap;
         mFlags = flags;
         mSyntheticModeManager = syntheticModeManager;
+        mDeviceStateManagerFlags = new FeatureFlagsImpl();
     }
 
     @Override
@@ -403,8 +409,8 @@
         // Retrieve the display info for the display that matches the display id.
         final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(display.getAddress());
         if (device == null) {
-            Slog.w(TAG, "The display device (" + display.getAddress() + "), is not available"
-                    + " for the display state " + mDeviceState);
+            Slog.w(TAG, "The display device (" + display.getAddress()
+                    + "), is not available for the display state " + mDeviceState.getIdentifier());
             return null;
         }
         LogicalDisplay logicalDisplay = getDisplayLocked(device, /* includeDisabled= */ true);
@@ -431,9 +437,11 @@
         ipw.println("mBootCompleted=" + mBootCompleted);
 
         ipw.println();
-        ipw.println("mDeviceState=" + mDeviceState);
-        ipw.println("mPendingDeviceState=" + mPendingDeviceState);
-        ipw.println("mDeviceStateToBeAppliedAfterBoot=" + mDeviceStateToBeAppliedAfterBoot);
+
+        ipw.println("mDeviceState=" + mDeviceState.getIdentifier());
+        ipw.println("mPendingDeviceState=" + mPendingDeviceState.getIdentifier());
+        ipw.println("mDeviceStateToBeAppliedAfterBoot="
+                + mDeviceStateToBeAppliedAfterBoot.getIdentifier());
 
         final int logicalDisplayCount = mLogicalDisplays.size();
         ipw.println();
@@ -463,7 +471,7 @@
         mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId);
     }
 
-    void setDeviceStateLocked(int state) {
+    void setDeviceStateLocked(DeviceState state) {
         if (!mBootCompleted) {
             // The boot animation might still be in progress, we do not want to switch states now
             // as the boot animation would end up with an incorrect size.
@@ -475,15 +483,17 @@
             return;
         }
 
-        Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
-                + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted);
+        Slog.i(TAG, "Requesting Transition to state: " + state + ", from state="
+                + mDeviceState.getIdentifier() + ", interactive=" + mInteractive
+                + ", mBootCompleted=" + mBootCompleted);
         // As part of a state transition, we may need to turn off some displays temporarily so that
         // the transition is smooth. Plus, on some devices, only one internal displays can be
         // on at a time. We use LogicalDisplay.setIsInTransition to mark a display that needs to be
         // temporarily turned off.
-        resetLayoutLocked(mDeviceState, state, /* transitionValue= */ true);
+        resetLayoutLocked(mDeviceState.getIdentifier(),
+                state.getIdentifier(), /* transitionValue= */ true);
         mPendingDeviceState = state;
-        mDeviceStateToBeAppliedAfterBoot = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+        mDeviceStateToBeAppliedAfterBoot = INVALID_DEVICE_STATE;
         final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState,
                 mInteractive, mBootCompleted);
         final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState,
@@ -498,7 +508,7 @@
         }
 
         if (DEBUG) {
-            Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState);
+            Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState.getIdentifier());
         }
         // Send the transitioning phase updates to DisplayManager so that the displays can
         // start turning OFF in preparation for the new layout.
@@ -533,8 +543,7 @@
     void onBootCompleted() {
         synchronized (mSyncRoot) {
             mBootCompleted = true;
-            if (mDeviceStateToBeAppliedAfterBoot
-                    != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER) {
+            if (!mDeviceStateToBeAppliedAfterBoot.equals(INVALID_DEVICE_STATE)) {
                 setDeviceStateLocked(mDeviceStateToBeAppliedAfterBoot);
             }
         }
@@ -563,11 +572,18 @@
      * @see #setDeviceStateLocked
      */
     @VisibleForTesting
-    boolean shouldDeviceBeWoken(int pendingState, int currentState, boolean isInteractive,
-            boolean isBootCompleted) {
-        return mDeviceStatesOnWhichToWakeUp.get(pendingState)
-                && !mDeviceStatesOnWhichToWakeUp.get(currentState)
-                && !isInteractive && isBootCompleted;
+    boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState,
+            boolean isInteractive, boolean isBootCompleted) {
+        if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+            return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
+                    && !currentState.equals(INVALID_DEVICE_STATE)
+                    && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
+                    && !isInteractive && isBootCompleted;
+        } else {
+            return mDeviceStatesOnWhichToWakeUp.get(pendingState.getIdentifier())
+                    && !mDeviceStatesOnWhichToWakeUp.get(currentState.getIdentifier())
+                    && !isInteractive && isBootCompleted;
+        }
     }
 
     /**
@@ -588,14 +604,22 @@
      * @see #setDeviceStateLocked
      */
     @VisibleForTesting
-    boolean shouldDeviceBePutToSleep(int pendingState, int currentState, boolean isInteractive,
-            boolean isBootCompleted) {
-        return currentState != DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER
-                && mDeviceStatesOnWhichToSelectiveSleep.get(pendingState)
-                && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState)
-                && isInteractive
-                && isBootCompleted
-                && !mFoldSettingProvider.shouldStayAwakeOnFold();
+    boolean shouldDeviceBePutToSleep(DeviceState pendingState, DeviceState currentState,
+            boolean isInteractive, boolean isBootCompleted) {
+        if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+            return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP)
+                    && !currentState.equals(INVALID_DEVICE_STATE)
+                    && !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP)
+                    && isInteractive
+                    && isBootCompleted
+                    && !mFoldSettingProvider.shouldStayAwakeOnFold();
+        } else {
+            return mDeviceStatesOnWhichToSelectiveSleep.get(pendingState.getIdentifier())
+                    && !mDeviceStatesOnWhichToSelectiveSleep.get(currentState.getIdentifier())
+                    && isInteractive
+                    && isBootCompleted
+                    && !mFoldSettingProvider.shouldStayAwakeOnFold();
+        }
     }
 
     private boolean areAllTransitioningDisplaysOffLocked() {
@@ -618,27 +642,25 @@
     }
 
     private void transitionToPendingStateLocked() {
-        resetLayoutLocked(mDeviceState, mPendingDeviceState, /* transitionValue= */ false);
+        resetLayoutLocked(mDeviceState.getIdentifier(),
+                mPendingDeviceState.getIdentifier(), /* transitionValue= */ false);
         mDeviceState = mPendingDeviceState;
-        mPendingDeviceState = DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+        mPendingDeviceState = INVALID_DEVICE_STATE;
         applyLayoutLocked();
         updateLogicalDisplaysLocked();
     }
 
     private void finishStateTransitionLocked(boolean force) {
-        if (mPendingDeviceState == DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER) {
+        if (mPendingDeviceState.equals(INVALID_DEVICE_STATE)) {
             return;
         }
 
-        final boolean waitingToWakeDevice = mDeviceStatesOnWhichToWakeUp.get(mPendingDeviceState)
-                && !mDeviceStatesOnWhichToWakeUp.get(mDeviceState)
-                && !mInteractive && mBootCompleted;
+        final boolean waitingToWakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState,
+                mInteractive, mBootCompleted);
         // The device should only wait for sleep if #shouldStayAwakeOnFold method returns false.
         // If not, device should be marked ready for transition immediately.
-        final boolean waitingToSleepDevice = mDeviceStatesOnWhichToSelectiveSleep.get(
-                mPendingDeviceState)
-                && !mDeviceStatesOnWhichToSelectiveSleep.get(mDeviceState)
-                && mInteractive && mBootCompleted && !shouldStayAwakeOnFold();
+        final boolean waitingToSleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState,
+                mDeviceState, mInteractive, mBootCompleted) && !shouldStayAwakeOnFold();
 
         final boolean displaysOff = areAllTransitioningDisplaysOffLocked();
         final boolean isReadyToTransition = displaysOff && !waitingToWakeDevice
@@ -1104,7 +1126,7 @@
      */
     private void applyLayoutLocked() {
         final Layout oldLayout = mCurrentLayout;
-        mCurrentLayout = mDeviceStateToLayoutMap.get(mDeviceState);
+        mCurrentLayout = mDeviceStateToLayoutMap.get(mDeviceState.getIdentifier());
         Slog.i(TAG, "Applying layout: " + mCurrentLayout + ", Previous layout: " + oldLayout);
 
         // Go through each of the displays in the current layout set.
@@ -1120,7 +1142,7 @@
             final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address);
             if (device == null) {
                 Slog.w(TAG, "applyLayoutLocked: The display device (" + address + "), is not "
-                        + "available for the display state " + mDeviceState);
+                        + "available for the display state " + mDeviceState.getIdentifier());
                 continue;
             }
 
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index 902daa4..5c2db35 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -133,7 +133,7 @@
             // new token not null and hdr min % of the screen is set, subscribe.
             // e.g. for virtual display, HBM data will be missing and HdrListener
             // should not be registered
-            if (displayToken != null && mHdrListener.mHdrMinPixels >= 0) {
+            if (displayToken != null && mHdrListener.mHdrMinPixels >= 0 && hasBrightnessLimits())  {
                 mHdrListener.register(displayToken);
                 mRegisteredDisplayToken = displayToken;
             }
@@ -179,6 +179,10 @@
         pw.println("  mAutoBrightnessEnabled=" + mAutoBrightnessEnabled);
     }
 
+    private boolean hasBrightnessLimits() {
+        return mHdrBrightnessData != null && !mHdrBrightnessData.maxBrightnessLimits.isEmpty();
+    }
+
     private void reset() {
         if (mMaxBrightness == PowerManager.BRIGHTNESS_MAX
                 && mDesiredMaxBrightness == PowerManager.BRIGHTNESS_MAX && mTransitionRate == -1f
@@ -214,11 +218,11 @@
             mDesiredMaxBrightness = expectedMaxBrightness;
             long debounceTime;
             if (mDesiredMaxBrightness > mMaxBrightness) {
-                debounceTime = mHdrBrightnessData.mBrightnessIncreaseDebounceMillis;
-                mDesiredTransitionRate = mHdrBrightnessData.mScreenBrightnessRampIncrease;
+                debounceTime = mHdrBrightnessData.brightnessIncreaseDebounceMillis;
+                mDesiredTransitionRate = mHdrBrightnessData.screenBrightnessRampIncrease;
             } else {
-                debounceTime = mHdrBrightnessData.mBrightnessDecreaseDebounceMillis;
-                mDesiredTransitionRate = mHdrBrightnessData.mScreenBrightnessRampDecrease;
+                debounceTime = mHdrBrightnessData.brightnessDecreaseDebounceMillis;
+                mDesiredTransitionRate = mHdrBrightnessData.screenBrightnessRampDecrease;
             }
 
             mHandler.removeCallbacks(mDebouncer);
@@ -232,7 +236,7 @@
         float foundAmbientBoundary = Float.MAX_VALUE;
         float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX;
         for (Map.Entry<Float, Float> brightnessPoint :
-                data.mMaxBrightnessLimits.entrySet()) {
+                data.maxBrightnessLimits.entrySet()) {
             float ambientBoundary = brightnessPoint.getKey();
             // find ambient lux upper boundary closest to current ambient lux
             if (ambientBoundary > ambientLux && ambientBoundary < foundAmbientBoundary) {
diff --git a/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java b/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java
new file mode 100644
index 0000000..5b4e8d5
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/DisplayDeviceConfigUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.function.Function;
+
+public class DisplayDeviceConfigUtils {
+    private static final String TAG = "DisplayDeviceConfigUtils";
+
+    /**
+     * Create Spline from generic data
+     * @param points - points for Spline in format (x0, y0), (x1, y1) etc
+     * @param xExtractor - extract X component from generic data
+     * @param yExtractor - extract Y component from generic data
+     */
+    @Nullable
+    public static <T> Spline createSpline(List<T> points, Function<T, BigDecimal> xExtractor,
+            Function<T, BigDecimal> yExtractor) {
+        int size = points.size();
+        if (size == 0) {
+            return null;
+        }
+
+        float[] x = new float[size];
+        float[] y = new float[size];
+
+        int i = 0;
+        for (T point : points) {
+            x[i] = xExtractor.apply(point).floatValue();
+            if (i > 0) {
+                if (x[i] <= x[i - 1]) {
+                    Slog.e(TAG, "spline control points must be strictly increasing, ignoring "
+                            + "configuration. x: " + x[i] + " <= " + x[i - 1]);
+                    return null;
+                }
+            }
+            y[i] = yExtractor.apply(point).floatValue();
+            ++i;
+        }
+
+        return Spline.createSpline(x, y);
+    }
+}
diff --git a/services/core/java/com/android/server/display/config/HdrBrightnessData.java b/services/core/java/com/android/server/display/config/HdrBrightnessData.java
index 837fbf7..c940807 100644
--- a/services/core/java/com/android/server/display/config/HdrBrightnessData.java
+++ b/services/core/java/com/android/server/display/config/HdrBrightnessData.java
@@ -16,63 +16,138 @@
 
 package com.android.server.display.config;
 
+import static com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
+
 import android.annotation.Nullable;
+import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.DisplayBrightnessState;
 
+import java.math.BigDecimal;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 /**
  * Brightness config for HDR content
+ * <pre>
+ * {@code
+ * <displayConfiguration>
+ *     ...
+ *     <hdrBrightnessConfig>
+ *         <brightnessMap>
+ *             <point>
+ *                 <first>500</first>
+ *                 <second>0.3</second>
+ *             </point>
+ *             <point>
+ *                 <first>1200</first>
+ *                 <second>0.6</second>
+ *             </point>
+ *         </brightnessMap>
+ *         <brightnessIncreaseDebounceMillis>1000</brightnessIncreaseDebounceMillis>
+ *         <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis>
+ *         <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis>
+ *         <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis>
+ *         <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm>
+ *         <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm>
+ *         <allowInLowPowerMode>true</allowInLowPowerMode>
+ *         <sdrHdrRatioMap>
+ *             <point>
+ *                 <first>2.0</first>
+ *                 <second>4.0</second>
+ *             </point>
+ *             <point>
+ *                 <first>100</first>
+ *                 <second>8.0</second>
+ *             </point>
+ *         </sdrHdrRatioMap>
+ *     </hdrBrightnessConfig>
+ *     ...
+ * </displayConfiguration>
+ * }
+ * </pre>
  */
 public class HdrBrightnessData {
+    private static final String TAG = "HdrBrightnessData";
 
     /**
      * Lux to brightness map
      */
-    public final Map<Float, Float> mMaxBrightnessLimits;
+    public final Map<Float, Float> maxBrightnessLimits;
 
     /**
      * Debounce time for brightness increase
      */
-    public final long mBrightnessIncreaseDebounceMillis;
+    public final long brightnessIncreaseDebounceMillis;
 
     /**
      * Brightness increase animation speed
      */
-    public final float mScreenBrightnessRampIncrease;
+    public final float screenBrightnessRampIncrease;
 
     /**
      * Debounce time for brightness decrease
      */
-    public final long mBrightnessDecreaseDebounceMillis;
+    public final long brightnessDecreaseDebounceMillis;
 
     /**
      * Brightness decrease animation speed
      */
-    public final float mScreenBrightnessRampDecrease;
+    public final float screenBrightnessRampDecrease;
+
+    /**
+     * Min Hdr layer size to start hdr brightness boost up to high brightness mode transition point
+     */
+    public final float minimumHdrPercentOfScreenForNbm;
+
+    /**
+     * Min Hdr layer size to start hdr brightness boost above high brightness mode transition point
+     */
+    public final float minimumHdrPercentOfScreenForHbm;
+
+    /**
+     * If Hdr brightness boost allowed in low power mode
+     */
+    public final boolean allowInLowPowerMode;
+
+    /**
+     * brightness to boost ratio spline
+     */
+    @Nullable
+    public final Spline sdrToHdrRatioSpline;
 
     @VisibleForTesting
     public HdrBrightnessData(Map<Float, Float> maxBrightnessLimits,
             long brightnessIncreaseDebounceMillis, float screenBrightnessRampIncrease,
-            long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease) {
-        mMaxBrightnessLimits = maxBrightnessLimits;
-        mBrightnessIncreaseDebounceMillis = brightnessIncreaseDebounceMillis;
-        mScreenBrightnessRampIncrease = screenBrightnessRampIncrease;
-        mBrightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis;
-        mScreenBrightnessRampDecrease = screenBrightnessRampDecrease;
+            long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease,
+            float minimumHdrPercentOfScreenForNbm, float minimumHdrPercentOfScreenForHbm,
+            boolean allowInLowPowerMode, @Nullable Spline sdrToHdrRatioSpline) {
+        this.maxBrightnessLimits = maxBrightnessLimits;
+        this.brightnessIncreaseDebounceMillis = brightnessIncreaseDebounceMillis;
+        this.screenBrightnessRampIncrease = screenBrightnessRampIncrease;
+        this.brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis;
+        this.screenBrightnessRampDecrease = screenBrightnessRampDecrease;
+        this.minimumHdrPercentOfScreenForNbm = minimumHdrPercentOfScreenForNbm;
+        this.minimumHdrPercentOfScreenForHbm = minimumHdrPercentOfScreenForHbm;
+        this.allowInLowPowerMode = allowInLowPowerMode;
+        this.sdrToHdrRatioSpline = sdrToHdrRatioSpline;
     }
 
     @Override
     public String toString() {
         return "HdrBrightnessData {"
-                + "mMaxBrightnessLimits: " + mMaxBrightnessLimits
-                + ", mBrightnessIncreaseDebounceMillis: " + mBrightnessIncreaseDebounceMillis
-                + ", mScreenBrightnessRampIncrease: " + mScreenBrightnessRampIncrease
-                + ", mBrightnessDecreaseDebounceMillis: " + mBrightnessDecreaseDebounceMillis
-                + ", mScreenBrightnessRampDecrease: " + mScreenBrightnessRampDecrease
+                + "mMaxBrightnessLimits: " + maxBrightnessLimits
+                + ", mBrightnessIncreaseDebounceMillis: " + brightnessIncreaseDebounceMillis
+                + ", mScreenBrightnessRampIncrease: " + screenBrightnessRampIncrease
+                + ", mBrightnessDecreaseDebounceMillis: " + brightnessDecreaseDebounceMillis
+                + ", mScreenBrightnessRampDecrease: " + screenBrightnessRampDecrease
+                + ", minimumHdrPercentOfScreenForNbm: " + minimumHdrPercentOfScreenForNbm
+                + ", minimumHdrPercentOfScreenForHbm: " + minimumHdrPercentOfScreenForHbm
+                + ", allowInLowPowerMode: " + allowInLowPowerMode
+                + ", sdrToHdrRatioSpline: " + sdrToHdrRatioSpline
                 + "} ";
     }
 
@@ -83,7 +158,7 @@
     public static HdrBrightnessData loadConfig(DisplayConfiguration config) {
         HdrBrightnessConfig hdrConfig = config.getHdrBrightnessConfig();
         if (hdrConfig == null) {
-            return null;
+            return getFallbackData(config.getHighBrightnessMode());
         }
 
         List<NonNegativeFloatToFloatPoint> points = hdrConfig.getBrightnessMap().getPoint();
@@ -92,10 +167,59 @@
             brightnessLimits.put(point.getFirst().floatValue(), point.getSecond().floatValue());
         }
 
+        float minHdrPercentForHbm = hdrConfig.getMinimumHdrPercentOfScreenForHbm() != null
+                ? hdrConfig.getMinimumHdrPercentOfScreenForHbm().floatValue()
+                : getFallbackHdrPercent(config.getHighBrightnessMode());
+
+        float minHdrPercentForNbm = hdrConfig.getMinimumHdrPercentOfScreenForNbm() != null
+                ? hdrConfig.getMinimumHdrPercentOfScreenForNbm().floatValue() : minHdrPercentForHbm;
+
         return new HdrBrightnessData(brightnessLimits,
                 hdrConfig.getBrightnessIncreaseDebounceMillis().longValue(),
                 hdrConfig.getScreenBrightnessRampIncrease().floatValue(),
                 hdrConfig.getBrightnessDecreaseDebounceMillis().longValue(),
-                hdrConfig.getScreenBrightnessRampDecrease().floatValue());
+                hdrConfig.getScreenBrightnessRampDecrease().floatValue(),
+                minHdrPercentForNbm, minHdrPercentForHbm, hdrConfig.getAllowInLowPowerMode(),
+                getSdrHdrRatioSpline(hdrConfig, config.getHighBrightnessMode()));
+    }
+
+    @Nullable
+    private static HdrBrightnessData getFallbackData(HighBrightnessMode hbm) {
+        if (hbm == null) {
+            return null;
+        }
+        float fallbackPercent = getFallbackHdrPercent(hbm);
+        Spline fallbackSpline = getFallbackSdrHdrRatioSpline(hbm);
+        return new HdrBrightnessData(Collections.emptyMap(),
+                0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET,
+                0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET,
+                fallbackPercent, fallbackPercent, false, fallbackSpline);
+    }
+
+    private static float getFallbackHdrPercent(HighBrightnessMode hbm) {
+        BigDecimal minHdrPctOfScreen = hbm != null ? hbm.getMinimumHdrPercentOfScreen_all() : null;
+        return minHdrPctOfScreen != null ? minHdrPctOfScreen.floatValue()
+                : HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
+    }
+
+    @Nullable
+    private static Spline getSdrHdrRatioSpline(HdrBrightnessConfig hdrConfig,
+            HighBrightnessMode hbm) {
+        NonNegativeFloatToFloatMap sdrHdrRatioMap = hdrConfig.getSdrHdrRatioMap();
+        if (sdrHdrRatioMap == null) {
+            return getFallbackSdrHdrRatioSpline(hbm);
+        }
+        return DisplayDeviceConfigUtils.createSpline(sdrHdrRatioMap.getPoint(),
+                NonNegativeFloatToFloatPoint::getFirst, NonNegativeFloatToFloatPoint::getSecond);
+    }
+
+    @Nullable
+    private static Spline getFallbackSdrHdrRatioSpline(HighBrightnessMode hbm) {
+        SdrHdrRatioMap fallbackMap = hbm != null ? hbm.getSdrHdrRatioMap_all() : null;
+        if (fallbackMap == null) {
+            return null;
+        }
+        return DisplayDeviceConfigUtils.createSpline(fallbackMap.getPoint(),
+                SdrHdrRatioPoint::getSdrNits, SdrHdrRatioPoint::getHdrRatio);
     }
 }
diff --git a/services/core/java/com/android/server/display/config/HighBrightnessModeData.java b/services/core/java/com/android/server/display/config/HighBrightnessModeData.java
new file mode 100644
index 0000000..dc2f976
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/HighBrightnessModeData.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+import android.view.SurfaceControl;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.math.BigDecimal;
+import java.util.function.Function;
+
+/**
+ * Container for high brightness mode configuration data.
+ */
+public class HighBrightnessModeData {
+    private static final String TAG = "HighBrightnessModeData";
+
+    static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f;
+
+    /** Minimum lux needed to enter high brightness mode */
+    public final float minimumLux;
+
+    /** Brightness level at which we transition from normal to high-brightness. */
+    public final float transitionPoint;
+
+    /** Whether HBM is allowed when {@code Settings.Global.LOW_POWER_MODE} is active. */
+    public final boolean allowInLowPowerMode;
+
+    /** Time window for HBM. */
+    public final long timeWindowMillis;
+
+    /** Maximum time HBM is allowed to be during in a {@code timeWindowMillis}. */
+    public final long timeMaxMillis;
+
+    /** Minimum time that HBM can be on before being enabled. */
+    public final long timeMinMillis;
+
+    /** Minimum HDR video size to enter high brightness mode */
+    public final float minimumHdrPercentOfScreen;
+
+    @Nullable
+    public final Spline sdrToHdrRatioSpline;
+
+    @Nullable
+    public final SurfaceControl.RefreshRateRange refreshRateLimit;
+
+    public final boolean isHighBrightnessModeEnabled;
+
+    @VisibleForTesting
+    public HighBrightnessModeData(float minimumLux, float transitionPoint, long timeWindowMillis,
+            long timeMaxMillis, long timeMinMillis, boolean allowInLowPowerMode,
+            float minimumHdrPercentOfScreen, @Nullable Spline sdrToHdrRatioSpline,
+            @Nullable SurfaceControl.RefreshRateRange refreshRateLimit,
+            boolean isHighBrightnessModeEnabled) {
+        this.minimumLux = minimumLux;
+        this.transitionPoint = transitionPoint;
+        this.timeWindowMillis = timeWindowMillis;
+        this.timeMaxMillis = timeMaxMillis;
+        this.timeMinMillis = timeMinMillis;
+        this.allowInLowPowerMode = allowInLowPowerMode;
+        this.minimumHdrPercentOfScreen = minimumHdrPercentOfScreen;
+        this.sdrToHdrRatioSpline = sdrToHdrRatioSpline;
+        this.refreshRateLimit = refreshRateLimit;
+        this.isHighBrightnessModeEnabled = isHighBrightnessModeEnabled;
+    }
+
+    @Override
+    public String toString() {
+        return "HBM{"
+                + "minLux: " + minimumLux
+                + ", transition: " + transitionPoint
+                + ", timeWindow: " + timeWindowMillis + "ms"
+                + ", timeMax: " + timeMaxMillis + "ms"
+                + ", timeMin: " + timeMinMillis + "ms"
+                + ", allowInLowPowerMode: " + allowInLowPowerMode
+                + ", minimumHdrPercentOfScreen: " + minimumHdrPercentOfScreen
+                + ", mSdrToHdrRatioSpline=" + sdrToHdrRatioSpline
+                + ", refreshRateLimit=" + refreshRateLimit
+                + ", isHighBrightnessModeEnabled=" + isHighBrightnessModeEnabled
+                + "} ";
+    }
+
+    /**
+     * Loads HighBrightnessModeData from DisplayConfiguration
+     */
+    public static HighBrightnessModeData loadHighBrightnessModeData(DisplayConfiguration config,
+            Function<HighBrightnessMode, Float> transitionPointProvider) {
+        final HighBrightnessMode hbm = config.getHighBrightnessMode();
+        float minimumLux = 0f;
+        float transitionPoint = 0f;
+        long timeWindowMillis = 0L;
+        long timeMaxMillis = 0L;
+        long timeMinMillis = 0L;
+        boolean allowInLowPowerMode = false;
+        float minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
+        Spline sdrToHdrRatioSpline = null;
+        SurfaceControl.RefreshRateRange refreshRateLimit = null;
+        boolean isEnabled = false;
+
+        if (hbm != null) {
+            minimumLux = hbm.getMinimumLux_all().floatValue();
+            transitionPoint = transitionPointProvider.apply(hbm);
+            HbmTiming hbmTiming = hbm.getTiming_all();
+            timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000;
+            timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000;
+            timeMinMillis = hbmTiming.getTimeMinSecs_all().longValue() * 1000;
+            allowInLowPowerMode = hbm.getAllowInLowPowerMode_all();
+            BigDecimal minHdrPctOfScreen = hbm.getMinimumHdrPercentOfScreen_all();
+            if (minHdrPctOfScreen != null) {
+                minimumHdrPercentOfScreen = minHdrPctOfScreen.floatValue();
+                if (minimumHdrPercentOfScreen > 1 || minimumHdrPercentOfScreen < 0) {
+                    Slog.w(TAG, "Invalid minimum HDR percent of screen: "
+                            + minimumHdrPercentOfScreen);
+                    minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
+                }
+            }
+
+            sdrToHdrRatioSpline = loadSdrHdrRatioMap(hbm);
+            RefreshRateRange rr = hbm.getRefreshRate_all();
+            if (rr != null) {
+                refreshRateLimit = new SurfaceControl.RefreshRateRange(
+                        rr.getMinimum().floatValue(), rr.getMaximum().floatValue());
+            }
+            isEnabled = hbm.getEnabled();
+        }
+        return new HighBrightnessModeData(minimumLux, transitionPoint,
+                timeWindowMillis, timeMaxMillis, timeMinMillis, allowInLowPowerMode,
+                minimumHdrPercentOfScreen, sdrToHdrRatioSpline, refreshRateLimit, isEnabled);
+
+    }
+
+    private static Spline loadSdrHdrRatioMap(HighBrightnessMode hbmConfig) {
+        final SdrHdrRatioMap sdrHdrRatioMap = hbmConfig.getSdrHdrRatioMap_all();
+        if (sdrHdrRatioMap == null) {
+            return null;
+        }
+        return DisplayDeviceConfigUtils.createSpline(sdrHdrRatioMap.getPoint(),
+                SdrHdrRatioPoint::getSdrNits, SdrHdrRatioPoint::getHdrRatio);
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
index 82ecb4a..91ab872 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
@@ -225,17 +225,17 @@
         sWriter.startThread();
     }
 
-    static void initialize(@NonNull Handler handler, @NonNull Context context) {
+    static void initialize(@NonNull Handler ioHandler, @NonNull Context context) {
         final UserManagerInternal userManagerInternal =
                 LocalServices.getService(UserManagerInternal.class);
-        handler.post(() -> {
+        ioHandler.post(() -> {
             userManagerInternal.addUserLifecycleListener(
                     new UserManagerInternal.UserLifecycleListener() {
                         @Override
                         public void onUserCreated(UserInfo user, @Nullable Object token) {
                             final int userId = user.id;
                             sWriter.onUserCreated(userId);
-                            handler.post(() -> {
+                            ioHandler.post(() -> {
                                 synchronized (ImfLock.class) {
                                     if (!sPerUserMap.contains(userId)) {
                                         final AdditionalSubtypeMap additionalSubtypeMap =
@@ -257,7 +257,7 @@
                         public void onUserRemoved(UserInfo user) {
                             final int userId = user.id;
                             sWriter.onUserRemoved(userId);
-                            handler.post(() -> {
+                            ioHandler.post(() -> {
                                 synchronized (ImfLock.class) {
                                     sPerUserMap.remove(userId);
                                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 875380f..7daf958 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -26,6 +26,7 @@
 import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED;
 import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION;
 import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CONCURRENT_MULTI_USER_MODE_ENABLED;
 import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE;
 import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_CLIENT;
 import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE;
@@ -78,6 +79,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.hardware.input.InputManager;
 import android.inputmethodservice.InputMethodService;
@@ -345,8 +347,8 @@
     private int mCurrentUserId;
 
     /** Holds all user related data */
-    @GuardedBy("ImfLock.class")
-    private UserDataRepository mUserDataRepository;
+    @SharedByAllUsersField
+    private final UserDataRepository mUserDataRepository;
 
     final WindowManagerInternal mWindowManagerInternal;
     private final ActivityManagerInternal mActivityManagerInternal;
@@ -922,11 +924,15 @@
      * {@link SystemService} used to publish and manage the lifecycle of
      * {@link InputMethodManagerService}.
      */
-    public static final class Lifecycle extends SystemService {
+    public static final class Lifecycle extends SystemService
+            implements UserManagerInternal.UserLifecycleListener {
         private final InputMethodManagerService mService;
 
         public Lifecycle(Context context) {
             this(context, createServiceForProduction(context));
+
+            // For production code, hook up user lifecycle
+            mService.mUserManagerInternal.addUserLifecycleListener(this);
         }
 
         @VisibleForTesting
@@ -958,6 +964,8 @@
                     Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
             ioThread.start();
 
+            SecureSettingsWrapper.setContentResolver(context.getContentResolver());
+
             return new InputMethodManagerService(context,
                     shouldEnableConcurrentMultiUserMode(context), thread.getLooper(),
                     Handler.createAsync(ioThread.getLooper()),
@@ -1005,6 +1013,19 @@
         }
 
         @Override
+        public void onUserCreated(UserInfo user, @Nullable Object token) {
+            // Called directly from UserManagerService. Do not block the calling thread.
+        }
+
+        @Override
+        public void onUserRemoved(UserInfo user) {
+            // Called directly from UserManagerService. Do not block the calling thread.
+            final int userId = user.id;
+            SecureSettingsWrapper.onUserRemoved(userId);
+            mService.mUserDataRepository.remove(userId);
+        }
+
+        @Override
         public void onUserUnlocking(@NonNull TargetUser user) {
             // Called on ActivityManager thread.
             SecureSettingsWrapper.onUserUnlocking(user.getUserIdentifier());
@@ -1017,13 +1038,15 @@
             // Called on ActivityManager thread.
             final int userId = user.getUserIdentifier();
             SecureSettingsWrapper.onUserStarting(userId);
-            synchronized (ImfLock.class) {
-                if (mService.mConcurrentMultiUserModeEnabled) {
-                    if (mService.mCurrentUserId != userId && mService.mSystemReady) {
-                        mService.initializeVisibleBackgroundUserLocked(userId);
+            mService.mIoHandler.post(() -> {
+                synchronized (ImfLock.class) {
+                    if (mService.mConcurrentMultiUserModeEnabled) {
+                        if (mService.mCurrentUserId != userId && mService.mSystemReady) {
+                            mService.initializeVisibleBackgroundUserLocked(userId);
+                        }
                     }
                 }
-            }
+            });
         }
 
     }
@@ -1081,7 +1104,6 @@
             mConcurrentMultiUserModeEnabled = concurrentMultiUserModeEnabled;
             mContext = context;
             mRes = context.getResources();
-            SecureSettingsWrapper.onStart(mContext);
 
             mHandler = Handler.createAsync(uiLooper, this);
             mIoHandler = ioHandler;
@@ -1106,14 +1128,14 @@
             // Search for InputMethodSettingsRepository.put() to find where and when it's actually
             // being updated. In general IMMS should refrain from exposing the existence of IMEs
             // until systemReady().
-            InputMethodSettingsRepository.initialize(mHandler, mContext);
-            AdditionalSubtypeMapRepository.initialize(mHandler, mContext);
+            InputMethodSettingsRepository.initialize(mIoHandler, mContext);
+            AdditionalSubtypeMapRepository.initialize(mIoHandler, mContext);
 
             mCurrentUserId = mActivityManagerInternal.getCurrentUserId();
             @SuppressWarnings("GuardedBy") final IntFunction<InputMethodBindingController>
                     bindingControllerFactory = userId -> new InputMethodBindingController(userId,
                     InputMethodManagerService.this);
-            mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal,
+            mUserDataRepository = new UserDataRepository(
                     bindingControllerForTesting != null ? bindingControllerForTesting
                             : bindingControllerFactory);
 
@@ -4560,6 +4582,7 @@
             proto.write(BACK_DISPOSITION, bindingController.getBackDisposition());
             proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis());
             proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard());
+            proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled);
             proto.end(token);
         }
     }
@@ -6013,6 +6036,7 @@
             final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
             final var userData = getUserData(userId);
             p.println("Current Input Method Manager state:");
+            p.println("  concurrentMultiUserModeEnabled" + mConcurrentMultiUserModeEnabled);
             final List<InputMethodInfo> methodList = settings.getMethodList();
             int numImes = methodList.size();
             p.println("  Input Methods:");
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
index 68924b5..a4d8ee5 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
@@ -54,16 +54,16 @@
         sPerUserMap.put(userId, obj);
     }
 
-    static void initialize(@NonNull Handler handler, @NonNull Context context) {
+    static void initialize(@NonNull Handler ioHandler, @NonNull Context context) {
         final UserManagerInternal userManagerInternal =
                 LocalServices.getService(UserManagerInternal.class);
-        handler.post(() -> {
+        ioHandler.post(() -> {
             userManagerInternal.addUserLifecycleListener(
                     new UserManagerInternal.UserLifecycleListener() {
                         @Override
                         public void onUserRemoved(UserInfo user) {
                             final int userId = user.id;
-                            handler.post(() -> {
+                            ioHandler.post(() -> {
                                 synchronized (ImfLock.class) {
                                     sPerUserMap.remove(userId);
                                 }
diff --git a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
index 4764e4f..e7cff20 100644
--- a/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
+++ b/services/core/java/com/android/server/inputmethod/SecureSettingsWrapper.java
@@ -20,10 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.app.ActivityManagerInternal;
 import android.content.ContentResolver;
-import android.content.Context;
-import android.content.pm.UserInfo;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -321,30 +318,13 @@
     }
 
     /**
-     * Called when {@link InputMethodManagerService} is starting.
+     * Called when the system is starting.
      *
-     * @param context the {@link Context} to be used.
+     * @param contentResolver the {@link ContentResolver} to be used
      */
     @AnyThread
-    static void onStart(@NonNull Context context) {
-        sContentResolver = context.getContentResolver();
-
-        final int userId = LocalServices.getService(ActivityManagerInternal.class)
-                .getCurrentUserId();
-        final UserManagerInternal userManagerInternal =
-                LocalServices.getService(UserManagerInternal.class);
-        putOrGet(userId, createImpl(userManagerInternal, userId));
-
-        userManagerInternal.addUserLifecycleListener(
-                new UserManagerInternal.UserLifecycleListener() {
-                    @Override
-                    public void onUserRemoved(UserInfo user) {
-                        synchronized (sUserMap) {
-                            sUserMap.remove(userId);
-                        }
-                    }
-                }
-        );
+    static void setContentResolver(@NonNull ContentResolver contentResolver) {
+        sContentResolver = contentResolver;
     }
 
     /**
@@ -377,6 +357,18 @@
     }
 
     /**
+     * Called when a user is being removed.
+     *
+     * @param userId the ID of the user whose storage is being removed.
+     */
+    @AnyThread
+    static void onUserRemoved(@UserIdInt int userId) {
+        synchronized (sUserMap) {
+            sUserMap.remove(userId);
+        }
+    }
+
+    /**
      * Put the given string {@code value} to {@code key}.
      *
      * @param key a secure settings key.
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 5cd980f..98d7548 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -16,11 +16,10 @@
 
 package com.android.server.inputmethod;
 
+import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.content.pm.UserInfo;
-import android.os.Handler;
 import android.util.SparseArray;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ImeTracker;
@@ -29,52 +28,63 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
 import com.android.internal.inputmethod.IRemoteInputConnection;
-import com.android.server.pm.UserManagerInternal;
 
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.function.Consumer;
 import java.util.function.IntFunction;
 
 final class UserDataRepository {
 
-    @GuardedBy("ImfLock.class")
+    private final ReentrantReadWriteLock mUserDataLock = new ReentrantReadWriteLock();
+
+    @GuardedBy("mUserDataLock")
     private final SparseArray<UserData> mUserData = new SparseArray<>();
 
     private final IntFunction<InputMethodBindingController> mBindingControllerFactory;
 
-    @GuardedBy("ImfLock.class")
+    @AnyThread
     @NonNull
     UserData getOrCreate(@UserIdInt int userId) {
-        UserData userData = mUserData.get(userId);
-        if (userData == null) {
-            userData = new UserData(userId, mBindingControllerFactory.apply(userId));
-            mUserData.put(userId, userData);
+        mUserDataLock.writeLock().lock();
+        try {
+            UserData userData = mUserData.get(userId);
+            if (userData == null) {
+                userData = new UserData(userId, mBindingControllerFactory.apply(userId));
+                mUserData.put(userId, userData);
+            }
+            return userData;
+        } finally {
+            mUserDataLock.writeLock().unlock();
         }
-        return userData;
     }
 
-    @GuardedBy("ImfLock.class")
+    @AnyThread
     void forAllUserData(Consumer<UserData> consumer) {
-        for (int i = 0; i < mUserData.size(); i++) {
-            consumer.accept(mUserData.valueAt(i));
+        final SparseArray<UserData> copiedArray;
+        mUserDataLock.readLock().lock();
+        try {
+            copiedArray = mUserData.clone();
+        } finally {
+            mUserDataLock.readLock().unlock();
+        }
+        for (int i = 0; i < copiedArray.size(); i++) {
+            consumer.accept(copiedArray.valueAt(i));
         }
     }
 
     UserDataRepository(
-            @NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal,
             @NonNull IntFunction<InputMethodBindingController> bindingControllerFactory) {
         mBindingControllerFactory = bindingControllerFactory;
-        userManagerInternal.addUserLifecycleListener(
-                new UserManagerInternal.UserLifecycleListener() {
-                    @Override
-                    public void onUserRemoved(UserInfo user) {
-                        final int userId = user.id;
-                        handler.post(() -> {
-                            synchronized (ImfLock.class) {
-                                mUserData.remove(userId);
-                            }
-                        });
-                    }
-                });
+    }
+
+    @AnyThread
+    void remove(@UserIdInt int userId) {
+        mUserDataLock.writeLock().lock();
+        try {
+            mUserData.remove(userId);
+        } finally {
+            mUserDataLock.writeLock().unlock();
+        }
     }
 
     /** Placeholder for all IMMS user specific fields */
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index ec5d96d..4a82057 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -106,11 +106,11 @@
     /**
      * Potentially log a zen mode change if the provided config and policy changes warrant it.
      *
-     * @param prevInfo    ZenModeInfo (zen mode setting, config, policy) prior to this change
-     * @param newInfo     ZenModeInfo after this change takes effect
-     * @param callingUid  the calling UID associated with the change; may be used to attribute the
-     *                    change to a particular package or determine if this is a user action
-     * @param origin      The origin of the Zen change.
+     * @param prevInfo   ZenModeInfo (zen mode setting, config, policy) prior to this change
+     * @param newInfo    ZenModeInfo after this change takes effect
+     * @param callingUid the calling UID associated with the change; may be used to attribute the
+     *                   change to a particular package or determine if this is a user action
+     * @param origin     The origin of the Zen change.
      */
     public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
             @ConfigChangeOrigin int origin) {
@@ -127,6 +127,9 @@
     /**
      * Reassign callingUid in mChangeState if we have more specific information that warrants it
      * (for instance, if the change is automatic and due to an automatic rule change).
+     *
+     * <p>When Flags.modesUi() is enabled, we reassign the calling UID to the package UID in all
+     * changes whose source is not system or system UI, as long as there is only one rule changed.
      */
     private void maybeReassignCallingUid() {
         int userId = Process.INVALID_UID;
@@ -145,12 +148,23 @@
             userId = mChangeState.mNewConfig.user;  // mNewConfig must not be null if enabler exists
         }
 
-        // The conditions where we should consider reassigning UID for an automatic rule change:
+        // The conditions where we should consider reassigning UID for an automatic rule change
+        // (pre-modes_ui):
         //   - we've determined it's not a user action
         //   - our current best guess is that the calling uid is system/sysui
+        // When Flags.modesUi() is true, we get the package UID for the changed rule, as long as:
+        //   - the change does not originate from the system based on change origin
+        //   - there is only one rule changed
         if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) {
-            if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) {
-                return;
+            if (Flags.modesUi()) {
+                // ignore anything whose origin is system
+                if (mChangeState.isFromSystemOrSystemUi()) {
+                    return;
+                }
+            } else {
+                if (mChangeState.getIsUserAction() || !mChangeState.isFromSystemOrSystemUi()) {
+                    return;
+                }
             }
 
             // Only try to get the package UID if there's exactly one changed automatic rule. If
@@ -202,7 +216,8 @@
                 /* int32 package_uid = 7 */ mChangeState.getPackageUid(),
                 /* DNDPolicyProto current_policy = 8 */ mChangeState.getDNDPolicyProto(),
                 /* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing(),
-                /* ActiveRuleType active_rule_types = 10 */ mChangeState.getActiveRuleTypes());
+                /* ActiveRuleType active_rule_types = 10 */ mChangeState.getActiveRuleTypes(),
+                /* ChangeOrigin change_origin = 11 */ mChangeState.getChangeOrigin());
     }
 
     /**
@@ -235,7 +250,8 @@
         ZenModeConfig mPrevConfig, mNewConfig;
         NotificationManager.Policy mPrevPolicy, mNewPolicy;
         int mCallingUid = Process.INVALID_UID;
-        @ConfigChangeOrigin int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
+        @ConfigChangeOrigin
+        int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
 
         private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
                 @ConfigChangeOrigin int origin) {
@@ -388,7 +404,8 @@
          * rules available.
          */
         @SuppressLint("WrongConstant")  // special case for log-only type on manual rule
-        @NonNull List<ZenRule> activeRulesList(ZenModeConfig config) {
+        @NonNull
+        List<ZenRule> activeRulesList(ZenModeConfig config) {
             ArrayList<ZenRule> rules = new ArrayList<>();
             if (config == null) {
                 return rules;
@@ -548,6 +565,17 @@
         }
 
         /**
+         * Get the config change origin associated with this change, which is stored in mOrigin.
+         * Only useable if modes_ui is true.
+         */
+        int getChangeOrigin() {
+            if (Flags.modesUi()) {
+                return mOrigin;
+            }
+            return 0;
+        }
+
+        /**
          * Convert the new policy to a DNDPolicyProto format for output in logs.
          *
          * <p>If {@code mNewZenMode} is {@code ZEN_MODE_OFF} (which can mean either no rules
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 8ee02dc4..303371b 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2629,18 +2629,28 @@
 
         String packageName = pkgLite.packageName;
         synchronized (mPm.mLock) {
-            // Package which currently owns the data that the new package will own if installed.
-            // If an app is uninstalled while keeping data (e.g. adb uninstall -k), installedPkg
-            // will be null whereas dataOwnerPkg will contain information about the package
-            // which was uninstalled while keeping its data.
-            AndroidPackage dataOwnerPkg = mPm.mPackages.get(packageName);
             PackageSetting dataOwnerPs = mPm.mSettings.getPackageLPr(packageName);
-            if (dataOwnerPkg  == null) {
-                if (dataOwnerPs != null) {
-                    dataOwnerPkg = dataOwnerPs.getPkg();
+            if (dataOwnerPs == null) {
+                if (requiredInstalledVersionCode != PackageManager.VERSION_CODE_HIGHEST) {
+                    String errorMsg = "Required installed version code was "
+                            + requiredInstalledVersionCode
+                            + " but package is not installed";
+                    Slog.w(TAG, errorMsg);
+                    return Pair.create(
+                            PackageManager.INSTALL_FAILED_WRONG_INSTALLED_VERSION, errorMsg);
                 }
+                // The package doesn't exist in the system, don't need to check the version
+                // replacing.
+                return Pair.create(PackageManager.INSTALL_SUCCEEDED, null);
             }
 
+            // Package which currently owns the data that the new package will own if installed.
+            // If an app is uninstalled while keeping data (e.g. adb uninstall -k), dataOwnerPkg
+            // will be null whereas dataOwnerPs will contain information about the package
+            // which was uninstalled while keeping its data. The AndroidPackage object that the
+            // PackageSetting refers to is the same object that is stored in mPackages.
+            AndroidPackage dataOwnerPkg = dataOwnerPs.getPkg();
+
             if (requiredInstalledVersionCode != PackageManager.VERSION_CODE_HIGHEST) {
                 if (dataOwnerPkg == null) {
                     String errorMsg = "Required installed version code was "
@@ -2662,7 +2672,27 @@
                 }
             }
 
-            if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
+            // If dataOwnerPkg is null but dataOwnerPs is not null, there is always data on
+            // some users. Wwe should do the downgrade check. E.g. DELETE_KEEP_DATA and
+            // archived apps
+            if (dataOwnerPkg == null) {
+                if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
+                        dataOwnerPs.isDebuggable())) {
+                    // The data exists on some users and downgrade is not permitted; a lower
+                    // version of the app will not be allowed.
+                    try {
+                        PackageManagerServiceUtils.checkDowngrade(dataOwnerPs, pkgLite);
+                    } catch (PackageManagerException e) {
+                        String errorMsg = "Downgrade detected on app uninstalled with"
+                                + " DELETE_KEEP_DATA: " + e.getMessage();
+                        Slog.w(TAG, errorMsg);
+                        return Pair.create(
+                                PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
+                    }
+                }
+                // dataOwnerPs.getPkg() is not null on system apps case. Don't need to consider
+                // system apps case like below.
+            } else if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
                 if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
                         dataOwnerPkg.isDebuggable())) {
                     // Downgrade is not permitted; a lower version of the app will not be allowed
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 924b36c..c3cac20 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -1419,10 +1419,23 @@
 
     /**
      * Check and throw if the given before/after packages would be considered a
-     * downgrade.
+     * downgrade with {@link PackageSetting}.
      */
-    public static void checkDowngrade(AndroidPackage before, PackageInfoLite after)
-            throws PackageManagerException {
+    public static void checkDowngrade(@NonNull PackageSetting before,
+            @NonNull PackageInfoLite after) throws PackageManagerException {
+        if (after.getLongVersionCode() < before.getVersionCode()) {
+            throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
+                    "Update version code " + after.versionCode + " is older than current "
+                            + before.getVersionCode());
+        }
+    }
+
+    /**
+     * Check and throw if the given before/after packages would be considered a
+     * downgrade with {@link AndroidPackage}.
+     */
+    public static void checkDowngrade(@NonNull AndroidPackage before,
+            @NonNull PackageInfoLite after) throws PackageManagerException {
         if (after.getLongVersionCode() < before.getLongVersionCode()) {
             throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
                     "Update version code " + after.versionCode + " is older than current "
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 7870b17..82df527 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -97,6 +97,7 @@
                 FORCE_QUERYABLE_OVERRIDE,
                 SCANNED_AS_STOPPED_SYSTEM_APP,
                 PENDING_RESTORE,
+                DEBUGGABLE,
         })
         public @interface Flags {
         }
@@ -105,6 +106,7 @@
         private static final int FORCE_QUERYABLE_OVERRIDE = 1 << 2;
         private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3;
         private static final int PENDING_RESTORE = 1 << 4;
+        private static final int DEBUGGABLE = 1 << 5;
     }
     private int mBooleans;
 
@@ -562,6 +564,20 @@
         return getBoolean(Booleans.PENDING_RESTORE);
     }
 
+    /**
+     * @see PackageState#isDebuggable
+     */
+    public PackageSetting setDebuggable(boolean value) {
+        setBoolean(Booleans.DEBUGGABLE, value);
+        onChanged();
+        return this;
+    }
+
+    @Override
+    public boolean isDebuggable() {
+        return getBoolean(Booleans.DEBUGGABLE);
+    }
+
     @Override
     public String toString() {
         return "PackageSetting{"
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 9ab6016..d8ce38e 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -437,6 +437,9 @@
             pkgSetting.setIsOrphaned(true);
         }
 
+        // update debuggable to packageSetting
+        pkgSetting.setDebuggable(parsedPackage.isDebuggable());
+
         // Take care of first install / last update times.
         final long scanFileTime = getLastModifiedTime(parsedPackage);
         final long existingFirstInstallTime = userId == UserHandle.USER_ALL
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 3956552..0d16b00 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3255,6 +3255,9 @@
         if (pkg.isPendingRestore()) {
             serializer.attributeBoolean(null, "pendingRestore", true);
         }
+        if (pkg.isDebuggable()) {
+            serializer.attributeBoolean(null, "debuggable", true);
+        }
         if (pkg.isLoading()) {
             serializer.attributeBoolean(null, "isLoading", true);
         }
@@ -3269,7 +3272,6 @@
         serializer.attributeInt(null, "appMetadataSource",
                 pkg.getAppMetadataSource());
 
-
         writeUsesSdkLibLPw(serializer, pkg.getUsesSdkLibraries(),
                 pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesOptional());
 
@@ -4059,6 +4061,7 @@
         long versionCode = 0;
         boolean installedForceQueryable = false;
         boolean isPendingRestore = false;
+        boolean isDebuggable = false;
         float loadingProgress = 0;
         long loadingCompletedTime = 0;
         UUID domainSetId;
@@ -4085,6 +4088,7 @@
             updateAvailable = parser.getAttributeBoolean(null, "updateAvailable", false);
             installedForceQueryable = parser.getAttributeBoolean(null, "forceQueryable", false);
             isPendingRestore = parser.getAttributeBoolean(null, "pendingRestore", false);
+            isDebuggable = parser.getAttributeBoolean(null, "debuggable", false);
             loadingProgress = parser.getAttributeFloat(null, "loadingProgress", 0);
             loadingCompletedTime = parser.getAttributeLongHex(null, "loadingCompletedTime", 0);
 
@@ -4259,6 +4263,7 @@
                     .setUpdateAvailable(updateAvailable)
                     .setForceQueryableOverride(installedForceQueryable)
                     .setPendingRestore(isPendingRestore)
+                    .setDebuggable(isDebuggable)
                     .setLoadingProgress(loadingProgress)
                     .setLoadingCompletedTime(loadingCompletedTime)
                     .setAppMetadataFilePath(appMetadataFilePath)
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 2c233f8..dde9943 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1176,6 +1176,30 @@
         }
     }
 
+    /** Marks the user as slated for deletion during boot if necessary. **/
+    @GuardedBy("mUsersLock")
+    private void markUserForRemovalIfNecessaryLU(UserInfo ui) {
+        if (!ui.isEphemeral()) {
+            // User should be ephemeral to be marked for removal.
+            return;
+        }
+        if (ui.preCreated) {
+            // Avoid marking pre-created users for removal.
+            return;
+        }
+        if (ui.lastLoggedInTime == 0 && ui.isGuest() && Resources.getSystem().getBoolean(
+                com.android.internal.R.bool.config_guestUserAutoCreated)) {
+            // Avoid marking auto-created but not-yet-logged-in guest user for removal. Because a
+            // new one will be created anyway, and this one doesn't have any personal data in it yet
+            // due to not being logged in.
+            return;
+        }
+        // Mark the user for removal.
+        addRemovingUserIdLocked(ui.id);
+        ui.partial = true;
+        ui.flags |= UserInfo.FLAG_DISABLED;
+    }
+
     /* Prunes out any partially created or partially removed users. */
     private void cleanupPartialUsers() {
         ArrayList<UserInfo> partials = new ArrayList<>();
@@ -4341,13 +4365,7 @@
                                             || mNextSerialNumber <= userData.info.id) {
                                         mNextSerialNumber = userData.info.id + 1;
                                     }
-                                    if (userData.info.isEphemeral() && !userData.info.preCreated
-                                            && userData.info.id != UserHandle.USER_SYSTEM) {
-                                        // Mark ephemeral user as slated for deletion.
-                                        addRemovingUserIdLocked(userData.info.id);
-                                        userData.info.partial = true;
-                                        userData.info.flags |= UserInfo.FLAG_DISABLED;
-                                    }
+                                    markUserForRemovalIfNecessaryLU(userData.info);
                                 }
                             }
                         } else if (name.equals(TAG_GUEST_RESTRICTIONS)) {
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index e0ee199..5876188 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -274,6 +274,14 @@
     boolean isPendingRestore();
 
     /**
+     * @see ApplicationInfo#FLAG_DEBUGGABLE
+     * @see R.styleable#AndroidManifestApplication_debuggable
+     * @see AndroidPackage#isDebuggable
+     * @hide
+     */
+    boolean isDebuggable();
+
+    /**
      * Retrieves the shared user app ID. Note that the actual shared user data is not available here
      * and must be queried separately.
      *
diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
index 3a79d0d..fde23b7 100644
--- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java
+++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
@@ -22,8 +22,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Icon;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -36,7 +38,11 @@
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.util.XmlUtils;
 import com.android.server.input.KeyboardMetricsCollector;
@@ -46,7 +52,9 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -183,8 +191,12 @@
                 String rolePackage = mRoleManager.getDefaultApplication(role);
                 if (rolePackage != null) {
                     intent = mPackageManager.getLaunchIntentForPackage(rolePackage);
-                    intent.putExtra(EXTRA_ROLE, role);
-                    mRoleIntents.put(role, intent);
+                    if (intent != null) {
+                        intent.putExtra(EXTRA_ROLE, role);
+                        mRoleIntents.put(role, intent);
+                    } else {
+                        Log.w(TAG, "No launch intent for role " + role);
+                    }
                 } else {
                     Log.w(TAG, "No default application for role " + role);
                 }
@@ -198,8 +210,7 @@
     private void loadShortcuts() {
 
         try {
-            XmlResourceParser parser = mContext.getResources().getXml(
-                    com.android.internal.R.xml.bookmarks);
+            XmlResourceParser parser = mContext.getResources().getXml(R.xml.bookmarks);
             XmlUtils.beginDocument(parser, TAG_BOOKMARKS);
 
             while (true) {
@@ -270,6 +281,9 @@
                         continue;
                     }
                     intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, categoryName);
+                    if (intent == null) {
+                        Log.w(TAG, "Null selector intent for " + categoryName);
+                    }
                 } else if (roleName != null) {
                     // We can't resolve the role at the time of this file being parsed as the
                     // device hasn't finished booting, so we will look it up lazily.
@@ -466,4 +480,131 @@
 
         return false;
     }
+
+    /**
+     * @param deviceId The input device id of the input device that will handle the shortcuts.
+     *
+     * @return a {@link KeyboardShortcutGroup} containing the application launch keyboard
+     *         shortcuts parsed at boot time from {@code bookmarks.xml}.
+     */
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        List<KeyboardShortcutInfo> shortcuts = new ArrayList();
+        for (int i = 0; i <  mIntentShortcuts.size(); i++) {
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mIntentShortcuts.keyAt(i)), mIntentShortcuts.valueAt(i), false);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mShiftShortcuts.size(); i++) {
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mShiftShortcuts.keyAt(i)), mShiftShortcuts.valueAt(i), true);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mRoleShortcuts.size(); i++) {
+            String role = mRoleShortcuts.valueAt(i);
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), false);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        for (int i = 0; i <  mShiftRoleShortcuts.size(); i++) {
+            String role = mShiftRoleShortcuts.valueAt(i);
+            KeyboardShortcutInfo info = shortcutInfoFromIntent(
+                    (char) (mShiftRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), true);
+            if (info != null) {
+                shortcuts.add(info);
+            }
+        }
+
+        return new KeyboardShortcutGroup(
+                mContext.getString(R.string.keyboard_shortcut_group_applications),
+                shortcuts);
+    }
+
+    /**
+     * Given an intent to launch an application and the character and shift state that should
+     * trigger it, return a suitable {@link KeyboardShortcutInfo} that contains the label and
+     * icon for the target application.
+     *
+     * @param baseChar the character that triggers the shortcut
+     * @param intent the application launch intent
+     * @param shift whether the shift key is required to be presed.
+     */
+    @VisibleForTesting
+    KeyboardShortcutInfo shortcutInfoFromIntent(char baseChar, Intent intent, boolean shift) {
+        if (intent == null) {
+            return null;
+        }
+
+        CharSequence label;
+        Icon icon;
+        ActivityInfo resolvedActivity = intent.resolveActivityInfo(
+                mPackageManager, PackageManager.MATCH_DEFAULT_ONLY);
+        if (resolvedActivity == null) {
+            return null;
+        }
+        boolean isResolver = com.android.internal.app.ResolverActivity.class.getName().equals(
+                resolvedActivity.name);
+        if (isResolver) {
+            label = getIntentCategoryLabel(mContext,
+                    intent.getSelector().getCategories().iterator().next());
+            if (label == null) {
+                return null;
+            }
+            icon = Icon.createWithResource(mContext, R.drawable.sym_def_app_icon);
+
+        } else {
+            label = resolvedActivity.loadLabel(mPackageManager);
+            icon = Icon.createWithResource(
+                    resolvedActivity.packageName, resolvedActivity.getIconResource());
+        }
+        int modifiers = KeyEvent.META_META_ON;
+        if (shift) {
+            modifiers |= KeyEvent.META_SHIFT_ON;
+        }
+        return new KeyboardShortcutInfo(label, icon, baseChar, modifiers);
+    }
+
+    @VisibleForTesting
+    static String getIntentCategoryLabel(Context context, CharSequence category) {
+        int resid;
+        switch (category.toString()) {
+            case Intent.CATEGORY_APP_BROWSER:
+                resid = R.string.keyboard_shortcut_group_applications_browser;
+                break;
+            case Intent.CATEGORY_APP_CONTACTS:
+                resid = R.string.keyboard_shortcut_group_applications_contacts;
+                break;
+            case Intent.CATEGORY_APP_EMAIL:
+                resid = R.string.keyboard_shortcut_group_applications_email;
+                break;
+            case Intent.CATEGORY_APP_CALENDAR:
+                resid = R.string.keyboard_shortcut_group_applications_calendar;
+                break;
+            case Intent.CATEGORY_APP_MAPS:
+                resid = R.string.keyboard_shortcut_group_applications_maps;
+                break;
+            case Intent.CATEGORY_APP_MUSIC:
+                resid = R.string.keyboard_shortcut_group_applications_music;
+                break;
+            case Intent.CATEGORY_APP_MESSAGING:
+                resid = R.string.keyboard_shortcut_group_applications_sms;
+                break;
+            case Intent.CATEGORY_APP_CALCULATOR:
+                resid = R.string.keyboard_shortcut_group_applications_calculator;
+                break;
+            default:
+                Log.e(TAG, ("No label for app category " + category));
+                return null;
+        }
+        return context.getString(resid);
+    };
+
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9d0c0e9..8dc9756 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -189,6 +189,7 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyCharacterMap.FallbackAction;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -3321,6 +3322,11 @@
                 eventToLog).sendToTarget();
     }
 
+    @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(deviceId);
+    }
+
     // TODO(b/117479243): handle it in InputPolicy
     // TODO (b/283241997): Add the remaining keyboard shortcut logging after refactoring
     /** {@inheritDoc} */
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 9ca4e27..6c05d70 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -82,6 +82,7 @@
 import android.view.Display;
 import android.view.IDisplayFoldListener;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowManagerPolicyConstants;
@@ -698,6 +699,15 @@
     public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags);
 
     /**
+     * Return the set of applicaition launch keyboard shortcuts the system supports.
+     *
+     * @param deviceId The id of the {@link InputDevice} that will trigger the shortcut.
+     *
+     * @return {@link KeyboardShortcutGroup} containing the shortcuts.
+     */
+    KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId);
+
+    /**
      * Called from the input reader thread before a motion is enqueued when the device is in a
      * non-interactive state.
      *
diff --git a/services/core/java/com/android/server/power/LowPowerStandbyController.java b/services/core/java/com/android/server/power/LowPowerStandbyController.java
index fa94b43..421471e 100644
--- a/services/core/java/com/android/server/power/LowPowerStandbyController.java
+++ b/services/core/java/com/android/server/power/LowPowerStandbyController.java
@@ -1380,10 +1380,7 @@
             Slog.d(TAG, "notifyStandbyPortsChanged");
         }
 
-        final Intent intent = new Intent(PowerManager.ACTION_LOW_POWER_STANDBY_PORTS_CHANGED);
-        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
-                Manifest.permission.MANAGE_LOW_POWER_STANDBY);
+        sendExplicitBroadcast(PowerManager.ACTION_LOW_POWER_STANDBY_PORTS_CHANGED);
     }
 
     /**
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 7e27407..f6c3d8e 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -76,6 +76,7 @@
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.PriorityQueue;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
@@ -109,12 +110,31 @@
     @GuardedBy("mChannelMapLock")
     private ArrayMap<Integer, TreeMap<Integer, ChannelItem>> mChannelMap;
 
+    /*
+     * Multi-level map storing the session statistics since last pull from StatsD.
+     * The first level is keyed by the UID of the process owning the session.
+     * The second level is keyed by the tag of the session. The point of separating different
+     * tags is that since different categories (e.g. HWUI vs APP) of the sessions may have different
+     * behaviors.
+     */
+    @GuardedBy("mSessionSnapshotMapLock")
+    private ArrayMap<Integer, ArrayMap<Integer, AppHintSessionSnapshot>> mSessionSnapshotMap;
+
     /** Lock to protect mActiveSessions and the UidObserver. */
     private final Object mLock = new Object();
 
     /** Lock to protect mChannelMap. */
     private final Object mChannelMapLock = new Object();
 
+    /*
+     * Lock to protect mSessionSnapshotMap.
+     * Nested acquisition of mSessionSnapshotMapLock and mLock should be avoided.
+     * We should grab these separately.
+     * When we need to have nested acquisitions, we should always follow the order of acquiring
+     * mSessionSnapshotMapLock first then mLock.
+     */
+    private final Object mSessionSnapshotMapLock = new Object();
+
     @GuardedBy("mNonIsolatedTidsLock")
     private final Map<Integer, Set<Long>> mNonIsolatedTids;
 
@@ -135,6 +155,8 @@
     private int mPowerHalVersion;
     private final PackageManager mPackageManager;
 
+    private boolean mUsesFmq;
+
     private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint";
     private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";
 
@@ -162,6 +184,7 @@
         }
         mActiveSessions = new ArrayMap<>();
         mChannelMap = new ArrayMap<>();
+        mSessionSnapshotMap = new ArrayMap<>();
         mNativeWrapper = injector.createNativeWrapper();
         mNativeWrapper.halInit();
         mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
@@ -170,6 +193,7 @@
                 LocalServices.getService(ActivityManagerInternal.class));
         mPowerHal = injector.createIPower();
         mPowerHalVersion = 0;
+        mUsesFmq = false;
         if (mPowerHal != null) {
             try {
                 mPowerHalVersion = mPowerHal.getInterfaceVersion();
@@ -197,6 +221,134 @@
         }
     }
 
+    private class AppHintSessionSnapshot {
+        /*
+         * Per-Uid and Per-SessionTag snapshot that tracks metrics including
+         * number of created sessions, number of power efficienct sessions, and
+         * maximum number of threads in a session.
+         * Given that it's Per-SessionTag, each uid can have multiple snapshots.
+         */
+        int mCurrentSessionCount;
+        int mMaxConcurrentSession;
+        int mMaxThreadCount;
+        int mPowerEfficientSessionCount;
+
+        final int mTargetDurationNsCountPQSize = 100;
+        PriorityQueue<TargetDurationRecord> mTargetDurationNsCountPQ;
+
+        class TargetDurationRecord implements Comparable<TargetDurationRecord> {
+            long mTargetDurationNs;
+            long mTimestamp;
+            int mCount;
+            TargetDurationRecord(long targetDurationNs) {
+                mTargetDurationNs = targetDurationNs;
+                mTimestamp = System.currentTimeMillis();
+                mCount = 1;
+            }
+
+            @Override
+            public int compareTo(TargetDurationRecord t) {
+                int tCount = t.getCount();
+                int thisCount = this.getCount();
+                // Here we sort in the order of number of count in ascending order.
+                // i.e. the lowest count of target duration is at the head of the queue.
+                // Upon same count, the tiebreaker is the timestamp, the older item will be at the
+                // front of the queue.
+                if (tCount == thisCount) {
+                    return (t.getTimestamp() < this.getTimestamp()) ? 1 : -1;
+                }
+                return (tCount < thisCount) ? 1 : -1;
+            }
+            long getTargetDurationNs() {
+                return mTargetDurationNs;
+            }
+
+            int getCount() {
+                return mCount;
+            }
+
+            long getTimestamp() {
+                return mTimestamp;
+            }
+
+            void setCount(int count) {
+                mCount = count;
+            }
+
+            void setTimestamp() {
+                mTimestamp = System.currentTimeMillis();
+            }
+
+            void setTargetDurationNs(long targetDurationNs) {
+                mTargetDurationNs = targetDurationNs;
+            }
+        }
+
+        AppHintSessionSnapshot() {
+            mCurrentSessionCount = 0;
+            mMaxConcurrentSession = 0;
+            mMaxThreadCount = 0;
+            mPowerEfficientSessionCount = 0;
+            mTargetDurationNsCountPQ = new PriorityQueue<>(1);
+        }
+
+        void updateUponSessionCreation(int threadCount, long targetDuration) {
+            mCurrentSessionCount += 1;
+            mMaxConcurrentSession = Math.max(mMaxConcurrentSession, mCurrentSessionCount);
+            mMaxThreadCount = Math.max(mMaxThreadCount, threadCount);
+            updateTargetDurationNs(targetDuration);
+        }
+
+        void updateUponSessionClose() {
+            mCurrentSessionCount -= 1;
+        }
+
+        void logPowerEfficientSession() {
+            mPowerEfficientSessionCount += 1;
+        }
+
+        void updateThreadCount(int threadCount) {
+            mMaxThreadCount = Math.max(mMaxThreadCount, threadCount);
+        }
+
+        void updateTargetDurationNs(long targetDurationNs) {
+            for (TargetDurationRecord t : mTargetDurationNsCountPQ) {
+                if (t.getTargetDurationNs() == targetDurationNs) {
+                    t.setCount(t.getCount() + 1);
+                    t.setTimestamp();
+                    return;
+                }
+            }
+            if (mTargetDurationNsCountPQ.size() == mTargetDurationNsCountPQSize) {
+                mTargetDurationNsCountPQ.poll();
+            }
+            mTargetDurationNsCountPQ.add(new TargetDurationRecord(targetDurationNs));
+        }
+
+        int getMaxConcurrentSession() {
+            return mMaxConcurrentSession;
+        }
+
+        int getMaxThreadCount() {
+            return mMaxThreadCount;
+        }
+
+        int getPowerEfficientSessionCount() {
+            return mPowerEfficientSessionCount;
+        }
+
+        long[] targetDurationNsList() {
+            final int listSize = 5;
+            long[] targetDurations = new long[listSize];
+            while (mTargetDurationNsCountPQ.size() > listSize) {
+                mTargetDurationNsCountPQ.poll();
+            }
+            for (int i = 0; i < listSize && !mTargetDurationNsCountPQ.isEmpty(); ++i) {
+                targetDurations[i] = mTargetDurationNsCountPQ.poll().getTargetDurationNs();
+            }
+            return targetDurations;
+        }
+    }
     private boolean isHalSupported() {
         return mHintSessionPreferredRate != -1;
     }
@@ -235,6 +387,11 @@
                 null, // use default PullAtomMetadata values
                 DIRECT_EXECUTOR,
                 this::onPullAtom);
+        statsManager.setPullAtomCallback(
+                FrameworkStatsLog.ADPF_SESSION_SNAPSHOT,
+                null, // use default PullAtomMetadata values
+                DIRECT_EXECUTOR,
+                this::onPullAtom);
     }
 
     private int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) {
@@ -247,11 +404,82 @@
             data.add(FrameworkStatsLog.buildStatsEvent(
                     FrameworkStatsLog.ADPF_SYSTEM_COMPONENT_INFO,
                     isSurfaceFlingerUsingCpuHint,
-                    isHwuiHintManagerEnabled));
+                    isHwuiHintManagerEnabled,
+                    getFmqUsage()));
+        }
+        if (atomTag == FrameworkStatsLog.ADPF_SESSION_SNAPSHOT) {
+            synchronized (mSessionSnapshotMapLock) {
+                for (int i = 0; i < mSessionSnapshotMap.size(); ++i) {
+                    final int uid = mSessionSnapshotMap.keyAt(i);
+                    final ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                            mSessionSnapshotMap.valueAt(i);
+                    for (int j = 0; j < sessionSnapshots.size(); ++j) {
+                        final int sessionTag = sessionSnapshots.keyAt(j);
+                        final AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.valueAt(j);
+                        data.add(FrameworkStatsLog.buildStatsEvent(
+                                FrameworkStatsLog.ADPF_SESSION_SNAPSHOT,
+                                uid,
+                                sessionTag,
+                                sessionSnapshot.getMaxConcurrentSession(),
+                                sessionSnapshot.getMaxThreadCount(),
+                                sessionSnapshot.getPowerEfficientSessionCount(),
+                                sessionSnapshot.targetDurationNsList()
+                        ));
+                    }
+                }
+            }
+            restoreSessionSnapshot();
         }
         return android.app.StatsManager.PULL_SUCCESS;
     }
 
+    private int getFmqUsage() {
+        if (mUsesFmq) {
+            return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__SUPPORTED;
+        } else if (mPowerHalVersion < 5) {
+            return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__HAL_VERSION_NOT_MET;
+        } else {
+            return FrameworkStatsLog.ADPFSYSTEM_COMPONENT_INFO__FMQ_SUPPORTED__UNSUPPORTED;
+        }
+    }
+
+    private void restoreSessionSnapshot() {
+        // clean up snapshot map and rebuild with current active sessions
+        synchronized (mSessionSnapshotMapLock) {
+            mSessionSnapshotMap.clear();
+            synchronized (mLock) {
+                for (int i = 0; i < mActiveSessions.size(); i++) {
+                    ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
+                            mActiveSessions.valueAt(i);
+                    for (int j = 0; j < tokenMap.size(); j++) {
+                        ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(j);
+                        for (int k = 0; k < sessionSet.size(); ++k) {
+                            AppHintSession appHintSession = sessionSet.valueAt(k);
+                            final int tag = appHintSession.getTag();
+                            final int uid = appHintSession.getUid();
+                            final long targetDuationNs =
+                                    appHintSession.getTargetDurationNs();
+                            final int threadCount = appHintSession.getThreadIds().length;
+                            ArrayMap<Integer, AppHintSessionSnapshot> snapshots =
+                                    mSessionSnapshotMap.get(uid);
+                            if (snapshots == null) {
+                                snapshots = new ArrayMap<>();
+                                mSessionSnapshotMap.put(uid, snapshots);
+                            }
+                            AppHintSessionSnapshot snapshot = snapshots.get(tag);
+                            if (snapshot == null) {
+                                snapshot = new AppHintSessionSnapshot();
+                                snapshots.put(tag, snapshot);
+                            }
+                            snapshot.updateUponSessionCreation(threadCount,
+                                    targetDuationNs);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Wrapper around the static-native methods from native.
      *
@@ -833,17 +1061,13 @@
                     // we change the session tag to SessionTag.GAME
                     // as it was not previously classified
                     switch (getUidApplicationCategory(callingUid)) {
-                        case ApplicationInfo.CATEGORY_GAME:
-                            tag = SessionTag.GAME;
-                            break;
-                        case ApplicationInfo.CATEGORY_UNDEFINED:
+                        case ApplicationInfo.CATEGORY_GAME -> tag = SessionTag.GAME;
+                        case ApplicationInfo.CATEGORY_UNDEFINED ->
                             // We use CATEGORY_UNDEFINED to filter the case when
                             // PackageManager.NameNotFoundException is caught,
                             // which should not happen.
                             tag = SessionTag.APP;
-                            break;
-                        default:
-                            tag = SessionTag.APP;
+                        default -> tag = SessionTag.APP;
                     }
                 }
 
@@ -889,9 +1113,15 @@
                 logPerformanceHintSessionAtom(
                         callingUid, sessionId, durationNanos, tids, tag);
 
+                synchronized (mSessionSnapshotMapLock) {
+                    // Update session snapshot upon session creation
+                    mSessionSnapshotMap.computeIfAbsent(callingUid, k -> new ArrayMap<>())
+                            .computeIfAbsent(tag, k -> new AppHintSessionSnapshot())
+                            .updateUponSessionCreation(tids.length, durationNanos);
+                }
                 synchronized (mLock) {
-                    AppHintSession hs = new AppHintSession(callingUid, callingTgid, tids, token,
-                            halSessionPtr, durationNanos);
+                    AppHintSession hs = new AppHintSession(callingUid, callingTgid, tag, tids,
+                            token, halSessionPtr, durationNanos);
                     ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap =
                             mActiveSessions.get(callingUid);
                     if (tokenMap == null) {
@@ -904,6 +1134,8 @@
                         tokenMap.put(token, sessionSet);
                     }
                     sessionSet.add(hs);
+                    mUsesFmq = mUsesFmq || hasChannel(callingTgid, callingUid);
+
                     return hs;
                 }
             } finally {
@@ -996,6 +1228,7 @@
     final class AppHintSession extends IHintSession.Stub implements IBinder.DeathRecipient {
         protected final int mUid;
         protected final int mPid;
+        protected final int mTag;
         protected int[] mThreadIds;
         protected final IBinder mToken;
         protected long mHalSessionPtr;
@@ -1003,6 +1236,7 @@
         protected boolean mUpdateAllowedByProcState;
         protected int[] mNewThreadIds;
         protected boolean mPowerEfficient;
+        protected boolean mHasBeenPowerEfficient;
         protected boolean mShouldForcePause;
 
         private enum SessionModes {
@@ -1010,16 +1244,18 @@
         };
 
         protected AppHintSession(
-                int uid, int pid, int[] threadIds, IBinder token,
+                int uid, int pid, int sessionTag, int[] threadIds, IBinder token,
                 long halSessionPtr, long durationNanos) {
             mUid = uid;
             mPid = pid;
+            mTag = sessionTag;
             mToken = token;
             mThreadIds = threadIds;
             mHalSessionPtr = halSessionPtr;
             mTargetDurationNanos = durationNanos;
             mUpdateAllowedByProcState = true;
             mPowerEfficient = false;
+            mHasBeenPowerEfficient = false;
             mShouldForcePause = false;
             final boolean allowed = mUidObserver.isUidForeground(mUid);
             updateHintAllowedByProcState(allowed);
@@ -1056,6 +1292,20 @@
                 mNativeWrapper.halUpdateTargetWorkDuration(mHalSessionPtr, targetDurationNanos);
                 mTargetDurationNanos = targetDurationNanos;
             }
+            synchronized (mSessionSnapshotMapLock) {
+                ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                        mSessionSnapshotMap.get(mUid);
+                if (sessionSnapshots == null) {
+                    Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                    return;
+                }
+                AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                if (sessionSnapshot == null) {
+                    Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag " + mTag);
+                    return;
+                }
+                sessionSnapshot.updateTargetDurationNs(mTargetDurationNanos);
+            }
         }
 
         @Override
@@ -1108,6 +1358,20 @@
                 if (sessionSet.isEmpty()) tokenMap.remove(mToken);
                 if (tokenMap.isEmpty()) mActiveSessions.remove(mUid);
             }
+            synchronized (mSessionSnapshotMapLock) {
+                ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                        mSessionSnapshotMap.get(mUid);
+                if (sessionSnapshots == null) {
+                    Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                    return;
+                }
+                AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                if (sessionSnapshot == null) {
+                    Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag " + mTag);
+                    return;
+                }
+                sessionSnapshot.updateUponSessionClose();
+            }
             if (powerhintThreadCleanup()) {
                 synchronized (mNonIsolatedTidsLock) {
                     final int[] tids = getTidsInternal();
@@ -1191,6 +1455,21 @@
                     mShouldForcePause = false;
                 }
             }
+            synchronized (mSessionSnapshotMapLock) {
+                ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                        mSessionSnapshotMap.get(mUid);
+                if (sessionSnapshots == null) {
+                    Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                    return;
+                }
+                AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                if (sessionSnapshot == null) {
+                    Slogf.w(TAG, "Session snapshot is null for uid " + mUid + " and tag "
+                            + mTag);
+                    return;
+                }
+                sessionSnapshot.updateThreadCount(tids.length);
+            }
         }
 
         public int[] getThreadIds() {
@@ -1231,6 +1510,26 @@
                 }
                 mNativeWrapper.halSetMode(mHalSessionPtr, mode, enabled);
             }
+            if (enabled && (mode == SessionModes.POWER_EFFICIENCY.ordinal())) {
+                if (!mHasBeenPowerEfficient) {
+                    mHasBeenPowerEfficient = true;
+                    synchronized (mSessionSnapshotMapLock) {
+                        ArrayMap<Integer, AppHintSessionSnapshot> sessionSnapshots =
+                                mSessionSnapshotMap.get(mUid);
+                        if (sessionSnapshots == null) {
+                            Slogf.w(TAG, "Session snapshot map is null for uid " + mUid);
+                            return;
+                        }
+                        AppHintSessionSnapshot sessionSnapshot = sessionSnapshots.get(mTag);
+                        if (sessionSnapshot == null) {
+                            Slogf.w(TAG, "Session snapshot is null for uid " + mUid
+                                    + " and tag " + mTag);
+                            return;
+                        }
+                        sessionSnapshot.logPowerEfficientSession();
+                    }
+                }
+            }
         }
 
         @Override
@@ -1254,6 +1553,20 @@
             }
         }
 
+        public int getUid() {
+            return mUid;
+        }
+
+        public int getTag() {
+            return mTag;
+        }
+
+        public long getTargetDurationNs() {
+            synchronized (this) {
+                return mTargetDurationNanos;
+            }
+        }
+
         void validateWorkDuration(WorkDuration workDuration) {
             if (DEBUG) {
                 Slogf.d(TAG, "WorkDuration("
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index 6d519ee..90981ada 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -86,8 +86,7 @@
     private ConsumedEnergyRetriever mConsumedEnergyRetriever;
     private IntSupplier mVoltageSupplier;
     private int[] mEnergyConsumerIds = new int[0];
-    private WifiActivityEnergyInfo mLastWifiActivityInfo =
-            new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0);
+    private WifiActivityEnergyInfo mLastWifiActivityInfo;
     private NetworkStats mLastNetworkStats;
     private long[] mLastConsumedEnergyUws;
     private int mLastVoltageMv;
@@ -206,14 +205,21 @@
             return null;
         }
 
-        long rxDuration = activityInfo.getControllerRxDurationMillis()
-                - mLastWifiActivityInfo.getControllerRxDurationMillis();
-        long txDuration = activityInfo.getControllerTxDurationMillis()
-                - mLastWifiActivityInfo.getControllerTxDurationMillis();
-        long scanDuration = activityInfo.getControllerScanDurationMillis()
-                - mLastWifiActivityInfo.getControllerScanDurationMillis();
-        long idleDuration = activityInfo.getControllerIdleDurationMillis()
-                - mLastWifiActivityInfo.getControllerIdleDurationMillis();
+        long rxDuration = 0;
+        long txDuration = 0;
+        long scanDuration = 0;
+        long idleDuration = 0;
+
+        if (mLastWifiActivityInfo != null) {
+            rxDuration = activityInfo.getControllerRxDurationMillis()
+                    - mLastWifiActivityInfo.getControllerRxDurationMillis();
+            txDuration = activityInfo.getControllerTxDurationMillis()
+                    - mLastWifiActivityInfo.getControllerTxDurationMillis();
+            scanDuration = activityInfo.getControllerScanDurationMillis()
+                    - mLastWifiActivityInfo.getControllerScanDurationMillis();
+            idleDuration = activityInfo.getControllerIdleDurationMillis()
+                    - mLastWifiActivityInfo.getControllerIdleDurationMillis();
+        }
 
         mLayout.setDeviceRxTime(mDeviceStats, rxDuration);
         mLayout.setDeviceTxTime(mDeviceStats, txDuration);
diff --git a/services/core/java/com/android/server/updates/Android.bp b/services/core/java/com/android/server/updates/Android.bp
new file mode 100644
index 0000000..10beebb
--- /dev/null
+++ b/services/core/java/com/android/server/updates/Android.bp
@@ -0,0 +1,11 @@
+aconfig_declarations {
+    name: "updates_flags",
+    package: "com.android.server.updates",
+    container: "system",
+    srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "updates_flags_lib",
+    aconfig_declarations: "updates_flags",
+}
diff --git a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
index 5565b6f..af4025e 100644
--- a/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertificateTransparencyLogInstallReceiver.java
@@ -16,17 +16,15 @@
 
 package com.android.server.updates;
 
+import android.content.Context;
+import android.content.Intent;
 import android.os.FileUtils;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.util.Base64;
 import android.util.Slog;
 
-import com.android.internal.util.HexDump;
-
 import libcore.io.Streams;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -36,10 +34,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 
 public class CertificateTransparencyLogInstallReceiver extends ConfigUpdateInstallReceiver {
 
@@ -52,31 +47,31 @@
 
     @Override
     protected void install(InputStream inputStream, int version) throws IOException {
-        /* Install is complicated here because we translate the input, which is a JSON file
-         * containing log information to a directory with a file per log. To support atomically
-         * replacing the old configuration directory with the new there's a bunch of steps. We
-         * create a new directory with the logs and then do an atomic update of the current symlink
-         * to point to the new directory.
-         */
+        if (!Flags.certificateTransparencyInstaller()) {
+            return;
+        }
+        // To support atomically replacing the old configuration directory with the new there's a
+        // bunch of steps. We create a new directory with the logs and then do an atomic update of
+        // the current symlink to point to the new directory.
         // 1. Ensure that the update dir exists and is readable
         updateDir.mkdir();
         if (!updateDir.isDirectory()) {
             throw new IOException("Unable to make directory " + updateDir.getCanonicalPath());
         }
         if (!updateDir.setReadable(true, false)) {
-            throw new IOException("Unable to set permissions on " +
-                    updateDir.getCanonicalPath());
+            throw new IOException("Unable to set permissions on " + updateDir.getCanonicalPath());
         }
         File currentSymlink = new File(updateDir, "current");
         File newVersion = new File(updateDir, LOGDIR_PREFIX + String.valueOf(version));
-        File oldDirectory;
         // 2. Handle the corner case where the new directory already exists.
         if (newVersion.exists()) {
             // If the symlink has already been updated then the update died between steps 7 and 8
             // and so we cannot delete the directory since its in use. Instead just bump the version
             // and return.
             if (newVersion.getCanonicalPath().equals(currentSymlink.getCanonicalPath())) {
-                writeUpdate(updateDir, updateVersion,
+                writeUpdate(
+                        updateDir,
+                        updateVersion,
                         new ByteArrayInputStream(Long.toString(version).getBytes()));
                 deleteOldLogDirectories();
                 return;
@@ -91,22 +86,12 @@
                 throw new IOException("Unable to make directory " + newVersion.getCanonicalPath());
             }
             if (!newVersion.setReadable(true, false)) {
-                throw new IOException("Failed to set " +newVersion.getCanonicalPath() +
-                        " readable");
+                throw new IOException(
+                        "Failed to set " + newVersion.getCanonicalPath() + " readable");
             }
 
-            // 4. For each log in the log file create the corresponding file in <new_version>/ .
-            try {
-                byte[] content = Streams.readFullyNoClose(inputStream);
-                JSONObject json = new JSONObject(new String(content, StandardCharsets.UTF_8));
-                JSONArray logs = json.getJSONArray("logs");
-                for (int i = 0; i < logs.length(); i++) {
-                    JSONObject log = logs.getJSONObject(i);
-                    installLog(newVersion, log);
-                }
-            } catch (JSONException e) {
-                throw new IOException("Failed to parse logs", e);
-            }
+            // 4. Validate the log list json and move the file in <new_version>/ .
+            installLogList(newVersion, inputStream);
 
             // 5. Create the temp symlink. We'll rename this to the target symlink to get an atomic
             // update.
@@ -125,49 +110,38 @@
         }
         Slog.i(TAG, "CT log directory updated to " + newVersion.getAbsolutePath());
         // 7. Update the current version information
-        writeUpdate(updateDir, updateVersion,
+        writeUpdate(
+                updateDir,
+                updateVersion,
                 new ByteArrayInputStream(Long.toString(version).getBytes()));
         // 8. Cleanup
         deleteOldLogDirectories();
     }
 
-    private void installLog(File directory, JSONObject logObject) throws IOException {
+    @Override
+    protected void postInstall(Context context, Intent intent) {
+        if (!Flags.certificateTransparencyInstaller()) {
+            return;
+        }
+    }
+
+    private void installLogList(File directory, InputStream inputStream) throws IOException {
         try {
-            String logFilename = getLogFileName(logObject.getString("key"));
-            File file = new File(directory, logFilename);
-            try (OutputStreamWriter out =
-                    new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
-                writeLogEntry(out, "key", logObject.getString("key"));
-                writeLogEntry(out, "url", logObject.getString("url"));
-                writeLogEntry(out, "description", logObject.getString("description"));
+            byte[] content = Streams.readFullyNoClose(inputStream);
+            if (new JSONObject(new String(content, StandardCharsets.UTF_8)).length() == 0) {
+                throw new IOException("Log list data not valid");
+            }
+
+            File file = new File(directory, "log_list.json");
+            try (FileOutputStream outputStream = new FileOutputStream(file)) {
+                outputStream.write(content);
             }
             if (!file.setReadable(true, false)) {
                 throw new IOException("Failed to set permissions on " + file.getCanonicalPath());
             }
         } catch (JSONException e) {
-            throw new IOException("Failed to parse log", e);
+            throw new IOException("Malformed json in log list", e);
         }
-
-    }
-
-    /**
-     * Get the filename for a log based on its public key. This must be kept in sync with
-     * org.conscrypt.ct.CTLogStoreImpl.
-     */
-    private String getLogFileName(String base64PublicKey) {
-        byte[] keyBytes = Base64.decode(base64PublicKey, Base64.DEFAULT);
-        try {
-            byte[] id = MessageDigest.getInstance("SHA-256").digest(keyBytes);
-            return HexDump.toHexString(id, false);
-        } catch (NoSuchAlgorithmException e) {
-            // SHA-256 is guaranteed to be available.
-            throw new RuntimeException(e);
-        }
-    }
-
-    private void writeLogEntry(OutputStreamWriter out, String key, String value)
-            throws IOException {
-        out.write(key + ":" + value + "\n");
     }
 
     private void deleteOldLogDirectories() throws IOException {
@@ -175,12 +149,14 @@
             return;
         }
         File currentTarget = new File(updateDir, "current").getCanonicalFile();
-        FileFilter filter = new FileFilter() {
-            @Override
-            public boolean accept(File file) {
-                return !currentTarget.equals(file) && file.getName().startsWith(LOGDIR_PREFIX);
-            }
-        };
+        FileFilter filter =
+                new FileFilter() {
+                    @Override
+                    public boolean accept(File file) {
+                        return !currentTarget.equals(file)
+                                && file.getName().startsWith(LOGDIR_PREFIX);
+                    }
+                };
         for (File f : updateDir.listFiles(filter)) {
             FileUtils.deleteContentsAndDir(f);
         }
diff --git a/services/core/java/com/android/server/updates/flags.aconfig b/services/core/java/com/android/server/updates/flags.aconfig
new file mode 100644
index 0000000..476cb37
--- /dev/null
+++ b/services/core/java/com/android/server/updates/flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.updates"
+container: "system"
+
+flag {
+    name: "certificate_transparency_installer"
+    is_exported: true
+    namespace: "network_security"
+    description: "Enable certificate transparency installer for log list data"
+    bug: "319829948"
+}
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 5be5bc5..2c73412 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -47,7 +47,7 @@
 import static com.android.server.accessibility.AccessibilityTraceProto.WINDOW_MANAGER_SERVICE;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowTracing.WINSCOPE_EXT;
+import static com.android.server.wm.WindowTracingLegacy.WINSCOPE_EXT;
 
 import android.accessibilityservice.AccessibilityTrace;
 import android.animation.ObjectAnimator;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5e644d3..8768074 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -64,6 +64,7 @@
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
@@ -232,6 +233,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
 import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked;
+import static com.android.server.wm.DesktopModeLaunchParamsModifier.canEnterDesktopMode;
 import static com.android.server.wm.IdentifierProto.HASH_CODE;
 import static com.android.server.wm.IdentifierProto.TITLE;
 import static com.android.server.wm.IdentifierProto.USER_ID;
@@ -858,8 +860,6 @@
     /** The last set {@link DropInputMode} for this activity surface. */
     @DropInputMode
     private int mLastDropInputMode = DropInputMode.NONE;
-    /** Whether the input to this activity will be dropped during the current playing animation. */
-    private boolean mIsInputDroppedForAnimation;
 
     /**
      * Whether the application has desk mode resources. Calculated and cached when
@@ -1102,11 +1102,9 @@
         pw.println(prefix + "mLastReportedConfigurations:");
         mLastReportedConfiguration.dump(pw, prefix + "  ");
 
-        if (Flags.activityWindowInfoFlag()) {
-            pw.print(prefix);
-            pw.print("mLastReportedActivityWindowInfo=");
-            pw.println(mLastReportedActivityWindowInfo);
-        }
+        pw.print(prefix);
+        pw.print("mLastReportedActivityWindowInfo=");
+        pw.println(mLastReportedActivityWindowInfo);
 
         pw.print(prefix); pw.print("CurrentConfiguration="); pw.println(getConfiguration());
         if (!getRequestedOverrideConfiguration().equals(EMPTY)) {
@@ -1699,15 +1697,6 @@
         }
     }
 
-    /** Sets if all input will be dropped as a protection during the client-driven animation. */
-    void setDropInputForAnimation(boolean isInputDroppedForAnimation) {
-        if (mIsInputDroppedForAnimation == isInputDroppedForAnimation) {
-            return;
-        }
-        mIsInputDroppedForAnimation = isInputDroppedForAnimation;
-        updateUntrustedEmbeddingInputProtection();
-    }
-
     /**
      * Sets to drop input when obscured to activity if it is embedded in untrusted mode.
      *
@@ -1720,10 +1709,7 @@
         if (getSurfaceControl() == null) {
             return;
         }
-        if (mIsInputDroppedForAnimation) {
-            // Disable all input during the animation.
-            setDropInputMode(DropInputMode.ALL);
-        } else if (isEmbeddedInUntrustedMode()) {
+        if (isEmbeddedInUntrustedMode()) {
             // Set drop input to OBSCURED when untrusted embedded.
             setDropInputMode(DropInputMode.OBSCURED);
         } else {
@@ -3173,7 +3159,7 @@
 
     @NonNull
     ActivityWindowInfo getActivityWindowInfo() {
-        if (!Flags.activityWindowInfoFlag() || !isAttached()) {
+        if (!isAttached()) {
             return mTmpActivityWindowInfo;
         }
         if (isFixedRotationTransforming()) {
@@ -8296,9 +8282,7 @@
     }
 
     void setLastReportedActivityWindowInfo(@NonNull ActivityWindowInfo activityWindowInfo) {
-        if (Flags.activityWindowInfoFlag()) {
-            mLastReportedActivityWindowInfo.set(activityWindowInfo);
-        }
+        mLastReportedActivityWindowInfo.set(activityWindowInfo);
     }
 
     @Nullable
@@ -9283,18 +9267,24 @@
     }
 
     void updateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
-        // Only allow to scale down.
         mSizeCompatScale = mAppCompatController.getTransparentPolicy()
                 .findOpaqueNotFinishingActivityBelow()
                 .map(activityRecord -> activityRecord.mSizeCompatScale)
-                .orElseGet(() -> {
-                    final int contentW = resolvedAppBounds.width();
-                    final int contentH = resolvedAppBounds.height();
-                    final int viewportW = containerAppBounds.width();
-                    final int viewportH = containerAppBounds.height();
-                    return (contentW <= viewportW && contentH <= viewportH) ? 1f : Math.min(
-                            (float) viewportW / contentW, (float) viewportH / contentH);
-                });
+                .orElseGet(() -> calculateSizeCompatScale(resolvedAppBounds, containerAppBounds));
+    }
+
+    private float calculateSizeCompatScale(Rect resolvedAppBounds, Rect containerAppBounds) {
+        final int contentW = resolvedAppBounds.width();
+        final int contentH = resolvedAppBounds.height();
+        final int viewportW = containerAppBounds.width();
+        final int viewportH = containerAppBounds.height();
+        // Allow an application to be up-scaled if its window is smaller than its
+        // original container or if it's a freeform window in desktop mode.
+        boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH)
+                || (canEnterDesktopMode(mAtmService.mContext)
+                    && getWindowingMode() == WINDOWING_MODE_FREEFORM);
+        return shouldAllowUpscaling ? Math.min(
+                (float) viewportW / contentW, (float) viewportH / contentH) : 1f;
     }
 
     private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) {
@@ -9708,8 +9698,8 @@
         // the combine configurations are equal, but would otherwise differ in the override config
         mTmpConfig.setTo(mLastReportedConfiguration.getMergedConfiguration());
         final ActivityWindowInfo newActivityWindowInfo = getActivityWindowInfo();
-        final boolean isActivityWindowInfoChanged = Flags.activityWindowInfoFlag()
-                && !mLastReportedActivityWindowInfo.equals(newActivityWindowInfo);
+        final boolean isActivityWindowInfoChanged =
+                !mLastReportedActivityWindowInfo.equals(newActivityWindowInfo);
         if (!displayChanged && !isActivityWindowInfoChanged
                 && getConfiguration().equals(mTmpConfig)) {
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Configuration & display "
@@ -9846,6 +9836,15 @@
      */
     private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
         int configChanged = info.getRealConfigChanged();
+        if (android.content.res.Flags.handleAllConfigChanges()) {
+            if ((configChanged & CONFIG_RESOURCES_UNUSED) != 0) {
+                // Don't relaunch any activities that claim they do not use resources at all.
+                // If they still do, the onConfigurationChanged() callback will get called to
+                // let them know anyway.
+                return false;
+            }
+        }
+
         boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig);
 
         // Override for apps targeting pre-O sdks
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5b17875..59b5da8 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5183,6 +5183,10 @@
             String hostingType) {
         if (!mStartingProcessActivities.contains(activity)) {
             mStartingProcessActivities.add(activity);
+            // Let the activity with higher z-order be started first.
+            if (mStartingProcessActivities.size() > 1) {
+                mStartingProcessActivities.sort(null /* by WindowContainer#compareTo */);
+            }
         } else if (mProcessNames.get(
                 activity.processName, activity.info.applicationInfo.uid) != null) {
             // The process is already starting. Wait for it to attach.
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 3f4bda7..54024e9 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -128,21 +128,24 @@
 
     // TODO(b/263368846) Rename when ASM logic is moved in
     @Retention(SOURCE)
-    @IntDef({BAL_BLOCK,
-            BAL_ALLOW_DEFAULT,
-            BAL_ALLOW_ALLOWLISTED_UID,
+    @IntDef({
             BAL_ALLOW_ALLOWLISTED_COMPONENT,
-            BAL_ALLOW_VISIBLE_WINDOW,
+            BAL_ALLOW_ALLOWLISTED_UID,
+            BAL_ALLOW_BOUND_BY_FOREGROUND,
+            BAL_ALLOW_DEFAULT,
+            BAL_ALLOW_FOREGROUND,
+            BAL_ALLOW_GRACE_PERIOD,
             BAL_ALLOW_PENDING_INTENT,
             BAL_ALLOW_PERMISSION,
             BAL_ALLOW_SAW_PERMISSION,
-            BAL_ALLOW_GRACE_PERIOD,
-            BAL_ALLOW_FOREGROUND,
-            BAL_ALLOW_SDK_SANDBOX
+            BAL_ALLOW_SDK_SANDBOX,
+            BAL_ALLOW_TOKEN,
+            BAL_ALLOW_VISIBLE_WINDOW,
+            BAL_BLOCK
     })
     public @interface BalCode {}
 
-    static final int BAL_BLOCK = 0;
+    static final int BAL_BLOCK = FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_BLOCKED;
 
     static final int BAL_ALLOW_DEFAULT =
             FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_DEFAULT;
@@ -195,10 +198,19 @@
     static final int BAL_ALLOW_NON_APP_VISIBLE_WINDOW =
             FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_NON_APP_VISIBLE_WINDOW;
 
+    /** Process belongs to a SDK sandbox */
+    static final int BAL_ALLOW_TOKEN =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_TOKEN;
+
+    /** Process belongs to a SDK sandbox */
+    static final int BAL_ALLOW_BOUND_BY_FOREGROUND =
+            FrameworkStatsLog.BAL_ALLOWED__ALLOWED_REASON__BAL_ALLOW_BOUND_BY_FOREGROUND;
+
     static String balCodeToString(@BalCode int balCode) {
         return switch (balCode) {
             case BAL_ALLOW_ALLOWLISTED_COMPONENT -> "BAL_ALLOW_ALLOWLISTED_COMPONENT";
             case BAL_ALLOW_ALLOWLISTED_UID -> "BAL_ALLOW_ALLOWLISTED_UID";
+            case BAL_ALLOW_BOUND_BY_FOREGROUND -> "BAL_ALLOW_BOUND_BY_FOREGROUND";
             case BAL_ALLOW_DEFAULT -> "BAL_ALLOW_DEFAULT";
             case BAL_ALLOW_FOREGROUND -> "BAL_ALLOW_FOREGROUND";
             case BAL_ALLOW_GRACE_PERIOD -> "BAL_ALLOW_GRACE_PERIOD";
@@ -207,6 +219,7 @@
             case BAL_ALLOW_PERMISSION -> "BAL_ALLOW_PERMISSION";
             case BAL_ALLOW_SAW_PERMISSION -> "BAL_ALLOW_SAW_PERMISSION";
             case BAL_ALLOW_SDK_SANDBOX -> "BAL_ALLOW_SDK_SANDBOX";
+            case BAL_ALLOW_TOKEN -> "BAL_ALLOW_TOKEN";
             case BAL_ALLOW_VISIBLE_WINDOW -> "BAL_ALLOW_VISIBLE_WINDOW";
             case BAL_BLOCK -> "BAL_BLOCK";
             default -> throw new IllegalArgumentException("Unexpected value: " + balCode);
@@ -1042,7 +1055,9 @@
                     || balCode == BAL_ALLOW_PENDING_INTENT
                     || balCode == BAL_ALLOW_SAW_PERMISSION
                     || balCode == BAL_ALLOW_VISIBLE_WINDOW
-                    || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) {
+                    || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW
+                    || balCode == BAL_ALLOW_TOKEN
+                    || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) {
                 return true;
             }
         }
@@ -1266,7 +1281,8 @@
                 || balCode == BAL_ALLOW_PERMISSION
                 || balCode == BAL_ALLOW_SAW_PERMISSION
                 || balCode == BAL_ALLOW_VISIBLE_WINDOW
-                || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW) {
+                || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW
+                || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) {
             return;
         }
 
@@ -1572,7 +1588,7 @@
         }
 
         if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW
-                || balCode == BAL_ALLOW_FOREGROUND) {
+                || balCode == BAL_ALLOW_FOREGROUND || balCode == BAL_ALLOW_BOUND_BY_FOREGROUND) {
             Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask;
             if (task != null && task.getDisplayArea() != null) {
                 joiner.add(prefix + "Tasks: ");
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index 478524b..4a870a3 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -23,10 +23,13 @@
 import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS;
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_DISALLOW;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BOUND_BY_FOREGROUND;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_TOKEN;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
+import static com.android.window.flags.Flags.balImprovedMetrics;
 
 import static java.util.Objects.requireNonNull;
 
@@ -110,8 +113,8 @@
         }
         // Allow if the flag was explicitly set.
         if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
-            return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
-                    "process allowed by token");
+            return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_TOKEN : BAL_ALLOW_PERMISSION,
+                    /*background*/ true, "process allowed by token");
         }
         // Allow if the caller is bound by a UID that's currently foreground.
         // But still respect the appSwitchState.
@@ -120,7 +123,8 @@
                 ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid()
                 : isBoundByForegroundUid();
         if (allowBoundByForegroundUid) {
-            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false,
+            return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_BOUND_BY_FOREGROUND
+                    : BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false,
                     "process bound by foreground uid");
         }
         // Allow if the caller has an activity in any foreground task.
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index a8edaeb..f8665c7 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -44,8 +44,8 @@
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.util.FastPrintWriter;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
 import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
@@ -53,7 +53,6 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.function.Consumer;
 
 /**
  * Helper class to run app animations in a remote process.
@@ -349,10 +348,6 @@
             } finally {
                 mIsFinishing = false;
             }
-            // Reset input for all activities when the remote animation is finished.
-            final Consumer<ActivityRecord> updateActivities =
-                    activity -> activity.setDropInputForAnimation(false);
-            mDisplayContent.forAllActivities(updateActivities);
         }
         setRunningRemoteAnimation(false);
         ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation");
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 4da8bbf..eb1a80b 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2583,8 +2583,12 @@
 
             // Containers don't belong to the same hierarchy???
             if (commonAncestor == null) {
-                throw new IllegalArgumentException("No in the same hierarchy this="
-                        + thisParentChain + " other=" + otherParentChain);
+                final int thisZ = getPrefixOrderIndex();
+                final int otherZ = other.getPrefixOrderIndex();
+                Slog.w(TAG, "Compare not in the same hierarchy this="
+                        + thisParentChain + " thisZ=" + thisZ + " other="
+                        + otherParentChain + " otherZ=" + otherZ);
+                return Integer.compare(thisZ, otherZ);
             }
 
             // Children are always considered greater than their parents, so if one of the containers
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8033122..700c069 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -283,6 +283,7 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.MagnificationSpec;
 import android.view.RemoteAnimationAdapter;
 import android.view.ScrollCaptureResponse;
@@ -334,8 +335,8 @@
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.internal.protolog.LegacyProtoLogImpl;
-import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FrameworkStatsLog;
@@ -7440,6 +7441,16 @@
     }
 
     @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        long token = Binder.clearCallingIdentity();
+        try {
+            return mPolicy.getApplicationLaunchKeyboardShortcuts(deviceId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
     public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
         enforceRegisterWindowManagerListenersPermission("requestAppKeyboardShortcuts");
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index de73e6c..228eb76 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1101,7 +1101,7 @@
         mPolicy = mWmService.mPolicy;
         mContext = mWmService.mContext;
         mForceSeamlesslyRotate = token.mRoundedCornerOverlay;
-        mLastReportedActivityWindowInfo = Flags.activityWindowInfoFlag() && mActivityRecord != null
+        mLastReportedActivityWindowInfo = mActivityRecord != null
                 ? new ActivityWindowInfo()
                 : null;
         mInputWindowHandle = new InputWindowHandleWrapper(new InputWindowHandle(
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index ba5323e..21f7eca 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -18,89 +18,52 @@
 
 import static android.os.Build.IS_USER;
 
-import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
-import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
 import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS;
 import static com.android.server.wm.WindowManagerTraceProto.WHERE;
 import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE;
 
 import android.annotation.Nullable;
 import android.os.ShellCommand;
-import android.os.SystemClock;
 import android.os.Trace;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
 
 import com.android.internal.protolog.LegacyProtoLogImpl;
-import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.ProtoLog;
-import com.android.internal.util.TraceBuffer;
 
-import java.io.File;
-import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * A class that allows window manager to dump its state continuously to a trace file, such that a
+ * A class that allows window manager to dump its state continuously, such that a
  * time series of window manager state can be analyzed after the fact.
  */
-class WindowTracing {
-
-    /**
-     * Maximum buffer size, currently defined as 5 MB
-     * Size was experimentally defined to fit between 100 to 150 elements.
-     */
-    private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB
-    private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB
-    private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB
-    static final String WINSCOPE_EXT = ".winscope";
-    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT;
-    private static final String TAG = "WindowTracing";
-    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+abstract class WindowTracing {
+    protected static final String TAG = "WindowTracing";
+    protected static final String WHERE_START_TRACING = "trace.enable";
+    protected static final String WHERE_ON_FRAME = "onFrame";
 
     private final WindowManagerService mService;
     private final Choreographer mChoreographer;
     private final WindowManagerGlobalLock mGlobalLock;
 
-    private final Object mEnabledLock = new Object();
-    private final File mTraceFile;
-    private final TraceBuffer mBuffer;
     private final Choreographer.FrameCallback mFrameCallback = (frameTimeNanos) ->
-            log("onFrame" /* where */);
+            log(WHERE_ON_FRAME);
 
-    private @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM;
-    private boolean mLogOnFrame = false;
-    private boolean mEnabled;
-    private volatile boolean mEnabledLockFree;
-    private boolean mScheduled;
+    private AtomicBoolean mScheduled = new AtomicBoolean(false);
 
-    private final IProtoLog mProtoLog;
 
     static WindowTracing createDefaultAndStartLooper(WindowManagerService service,
             Choreographer choreographer) {
-        File file = new File(TRACE_FILENAME);
-        return new WindowTracing(file, service, choreographer, BUFFER_CAPACITY_TRIM);
+        return new WindowTracingLegacy(service, choreographer);
     }
 
-    private WindowTracing(File file, WindowManagerService service, Choreographer choreographer,
-            int bufferCapacity) {
-        this(file, service, choreographer, service.mGlobalLock, bufferCapacity);
-    }
-
-    WindowTracing(File file, WindowManagerService service, Choreographer choreographer,
-            WindowManagerGlobalLock globalLock, int bufferCapacity) {
+    protected WindowTracing(WindowManagerService service, Choreographer choreographer,
+            WindowManagerGlobalLock globalLock) {
         mChoreographer = choreographer;
         mService = service;
         mGlobalLock = globalLock;
-        mTraceFile = file;
-        mBuffer = new TraceBuffer(bufferCapacity);
-        setLogLevel(WindowTraceLogLevel.TRIM, null /* pw */);
-        mProtoLog = ProtoLog.getSingleInstance();
     }
 
     void startTrace(@Nullable PrintWriter pw) {
@@ -108,44 +71,29 @@
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
-            }
-            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
-            mBuffer.resetBuffer();
-            mEnabled = mEnabledLockFree = true;
+        if (!android.tracing.Flags.perfettoProtologTracing()) {
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
         }
-        log("trace.enable");
+        startTraceInternal(pw);
     }
 
-    /**
-     * Stops the trace and write the current buffer to disk
-     * @param pw Print writer
-     */
     void stopTrace(@Nullable PrintWriter pw) {
         if (IS_USER) {
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
-            mEnabled = mEnabledLockFree = false;
-
-            if (mEnabled) {
-                logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
-                throw new IllegalStateException("tracing enabled while waiting for flush.");
-            }
-            writeTraceToFileLocked();
-            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
-        }
         if (!android.tracing.Flags.perfettoProtologTracing()) {
             ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true);
         }
+        stopTraceInternal(pw);
     }
 
     /**
-     * Stops the trace and write the current buffer to disk then restart, if it's already running.
+     * If legacy tracing is enabled (either WM or ProtoLog):
+     * 1. Stop tracing
+     * 2. Write trace to disk (to be picked by dumpstate)
+     * 3. Restart tracing
+     *
      * @param pw Print writer
      */
     void saveForBugreport(@Nullable PrintWriter pw) {
@@ -153,143 +101,24 @@
             logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
             return;
         }
-        synchronized (mEnabledLock) {
-            if (!mEnabled) {
-                return;
-            }
-            mEnabled = mEnabledLockFree = false;
-            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
-            writeTraceToFileLocked();
-            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) mProtoLog).stopProtoLog(pw, true);
-            }
-            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
-            mBuffer.resetBuffer();
-            mEnabled = mEnabledLockFree = true;
-            if (!android.tracing.Flags.perfettoProtologTracing()) {
-                ((LegacyProtoLogImpl) mProtoLog).startProtoLog(pw);
-            }
+        if (!android.tracing.Flags.perfettoProtologTracing()
+                && ProtoLog.getSingleInstance().isProtoEnabled()) {
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).stopProtoLog(pw, true);
+            ((LegacyProtoLogImpl) ProtoLog.getSingleInstance()).startProtoLog(pw);
         }
+        saveForBugreportInternal(pw);
     }
 
-    private void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing log level to " + logLevel);
-        mLogLevel = logLevel;
-
-        switch (logLevel) {
-            case WindowTraceLogLevel.ALL: {
-                setBufferCapacity(BUFFER_CAPACITY_ALL, pw);
-                break;
-            }
-            case WindowTraceLogLevel.TRIM: {
-                setBufferCapacity(BUFFER_CAPACITY_TRIM, pw);
-                break;
-            }
-            case WindowTraceLogLevel.CRITICAL: {
-                setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw);
-                break;
-            }
-        }
-    }
-
-    private void setLogFrequency(boolean onFrame, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing log frequency to "
-                + ((onFrame) ? "frame" : "transaction"));
-        mLogOnFrame = onFrame;
-    }
-
-    private void setBufferCapacity(int capacity, PrintWriter pw) {
-        logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes");
-        mBuffer.setCapacity(capacity);
-    }
-
-    boolean isEnabled() {
-        return mEnabledLockFree;
-    }
-
-    int onShellCommand(ShellCommand shell) {
-        PrintWriter pw = shell.getOutPrintWriter();
-        String cmd = shell.getNextArgRequired();
-        switch (cmd) {
-            case "start":
-                startTrace(pw);
-                return 0;
-            case "stop":
-                stopTrace(pw);
-                return 0;
-            case "save-for-bugreport":
-                saveForBugreport(pw);
-                return 0;
-            case "status":
-                logAndPrintln(pw, getStatus());
-                return 0;
-            case "frame":
-                setLogFrequency(true /* onFrame */, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            case "transaction":
-                setLogFrequency(false /* onFrame */, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            case "level":
-                String logLevelStr = shell.getNextArgRequired().toLowerCase();
-                switch (logLevelStr) {
-                    case "all": {
-                        setLogLevel(WindowTraceLogLevel.ALL, pw);
-                        break;
-                    }
-                    case "trim": {
-                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
-                        break;
-                    }
-                    case "critical": {
-                        setLogLevel(WindowTraceLogLevel.CRITICAL, pw);
-                        break;
-                    }
-                    default: {
-                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
-                        break;
-                    }
-                }
-                mBuffer.resetBuffer();
-                return 0;
-            case "size":
-                setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw);
-                mBuffer.resetBuffer();
-                return 0;
-            default:
-                pw.println("Unknown command: " + cmd);
-                pw.println("Window manager trace options:");
-                pw.println("  start: Start logging");
-                pw.println("  stop: Stop logging");
-                pw.println("  save-for-bugreport: Save logging data to file if it's running.");
-                pw.println("  frame: Log trace once per frame");
-                pw.println("  transaction: Log each transaction");
-                pw.println("  size: Set the maximum log size (in KB)");
-                pw.println("  status: Print trace status");
-                pw.println("  level [lvl]: Set the log level between");
-                pw.println("    lvl may be one of:");
-                pw.println("      critical: Only visible windows with reduced information");
-                pw.println("      trim: All windows with reduced");
-                pw.println("      all: All window and information");
-                return -1;
-        }
-    }
-
-    String getStatus() {
-        return "Status: "
-                + ((isEnabled()) ? "Enabled" : "Disabled")
-                + "\n"
-                + "Log level: "
-                + mLogLevel
-                + "\n"
-                + mBuffer.getStatus();
-    }
+    abstract void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw);
+    abstract void setLogFrequency(boolean onFrame, PrintWriter pw);
+    abstract void setBufferCapacity(int capacity, PrintWriter pw);
+    abstract boolean isEnabled();
+    abstract int onShellCommand(ShellCommand shell);
+    abstract String getStatus();
 
     /**
      * If tracing is enabled, log the current state or schedule the next frame to be logged,
-     * according to {@link #mLogOnFrame}.
+     * according to the configuration in the derived tracing class.
      *
      * @param where Logging point descriptor
      */
@@ -298,59 +127,63 @@
             return;
         }
 
-        if (mLogOnFrame) {
-            schedule();
-        } else {
+        if (shouldLogOnTransaction()) {
             log(where);
         }
+
+        if (shouldLogOnFrame()) {
+            schedule();
+        }
     }
 
     /**
      * Schedule the log to trace the next frame
      */
     private void schedule() {
-        if (mScheduled) {
+        if (!mScheduled.compareAndSet(false, true)) {
             return;
         }
 
-        mScheduled = true;
         mChoreographer.postFrameCallback(mFrameCallback);
     }
 
     /**
-     * Write the current frame to the buffer
+     * Write the current frame to proto
      *
+     * @param os Proto stream buffer
+     * @param logLevel Log level
      * @param where Logging point descriptor
+     * @param elapsedRealtimeNanos Timestamp
      */
-    private void log(String where) {
+    protected void dumpToProto(ProtoOutputStream os, @WindowTraceLogLevel int logLevel,
+            String where, long elapsedRealtimeNanos) {
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "traceStateLocked");
         try {
-            ProtoOutputStream os = new ProtoOutputStream();
-            long tokenOuter = os.start(ENTRY);
-            os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
+            os.write(ELAPSED_REALTIME_NANOS, elapsedRealtimeNanos);
             os.write(WHERE, where);
 
-            long tokenInner = os.start(WINDOW_MANAGER_SERVICE);
+            long token = os.start(WINDOW_MANAGER_SERVICE);
             synchronized (mGlobalLock) {
                 Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
                 try {
-                    mService.dumpDebugLocked(os, mLogLevel);
+                    mService.dumpDebugLocked(os, logLevel);
                 } finally {
                     Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
                 }
             }
-            os.end(tokenInner);
-            os.end(tokenOuter);
-            mBuffer.add(os);
-            mScheduled = false;
+            os.end(token);
         } catch (Exception e) {
             Log.wtf(TAG, "Exception while tracing state", e);
         } finally {
+            boolean isOnFrameLogEvent = where == WHERE_ON_FRAME;
+            if (isOnFrameLogEvent) {
+                mScheduled.set(false);
+            }
             Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         }
     }
 
-    private void logAndPrintln(@Nullable PrintWriter pw, String msg) {
+    protected void logAndPrintln(@Nullable PrintWriter pw, String msg) {
         Log.i(TAG, msg);
         if (pw != null) {
             pw.println(msg);
@@ -358,24 +191,10 @@
         }
     }
 
-    /**
-     * Writes the trace buffer to disk. This method has no internal synchronization and should be
-     * externally synchronized
-     */
-    private void writeTraceToFileLocked() {
-        try {
-            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked");
-            ProtoOutputStream proto = new ProtoOutputStream();
-            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
-            long timeOffsetNs =
-                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
-                    - SystemClock.elapsedRealtimeNanos();
-            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
-            mBuffer.writeTraceToFile(mTraceFile, proto);
-        } catch (IOException e) {
-            Log.e(TAG, "Unable to write buffer to file", e);
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
-        }
-    }
+    protected abstract void startTraceInternal(@Nullable PrintWriter pw);
+    protected abstract void stopTraceInternal(@Nullable PrintWriter pw);
+    protected abstract void saveForBugreportInternal(@Nullable PrintWriter pw);
+    protected abstract void log(String where);
+    protected abstract boolean shouldLogOnFrame();
+    protected abstract boolean shouldLogOnTransaction();
 }
diff --git a/services/core/java/com/android/server/wm/WindowTracingLegacy.java b/services/core/java/com/android/server/wm/WindowTracingLegacy.java
new file mode 100644
index 0000000..7a36707
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowTracingLegacy.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.WindowManagerTraceFileProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
+
+import android.annotation.Nullable;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.Choreographer;
+
+import com.android.internal.util.TraceBuffer;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.concurrent.TimeUnit;
+
+class WindowTracingLegacy extends WindowTracing {
+
+    /**
+     * Maximum buffer size, currently defined as 5 MB
+     * Size was experimentally defined to fit between 100 to 150 elements.
+     */
+    private static final int BUFFER_CAPACITY_CRITICAL = 5120 * 1024; // 5 MB
+    private static final int BUFFER_CAPACITY_TRIM = 10240 * 1024; // 10 MB
+    private static final int BUFFER_CAPACITY_ALL = 20480 * 1024; // 20 MB
+    static final String WINSCOPE_EXT = ".winscope";
+    private static final String TRACE_FILENAME = "/data/misc/wmtrace/wm_trace" + WINSCOPE_EXT;
+    private static final String TAG = "WindowTracing";
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final Object mEnabledLock = new Object();
+    private final File mTraceFile;
+    private final TraceBuffer mBuffer;
+
+    private boolean mEnabled;
+    private volatile boolean mEnabledLockFree;
+
+    protected @WindowTraceLogLevel int mLogLevel = WindowTraceLogLevel.TRIM;
+    protected boolean mLogOnFrame = false;
+
+    WindowTracingLegacy(WindowManagerService service, Choreographer choreographer) {
+        this(new File(TRACE_FILENAME), service, choreographer,
+                service.mGlobalLock, BUFFER_CAPACITY_TRIM);
+    }
+
+    WindowTracingLegacy(File traceFile, WindowManagerService service, Choreographer choreographer,
+            WindowManagerGlobalLock globalLock, int bufferSize) {
+        super(service, choreographer, globalLock);
+        mTraceFile = traceFile;
+        mBuffer = new TraceBuffer(bufferSize);
+    }
+
+    @Override
+    void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing log level to " + logLevel);
+        mLogLevel = logLevel;
+
+        switch (logLevel) {
+            case WindowTraceLogLevel.ALL: {
+                setBufferCapacity(BUFFER_CAPACITY_ALL, pw);
+                break;
+            }
+            case WindowTraceLogLevel.TRIM: {
+                setBufferCapacity(BUFFER_CAPACITY_TRIM, pw);
+                break;
+            }
+            case WindowTraceLogLevel.CRITICAL: {
+                setBufferCapacity(BUFFER_CAPACITY_CRITICAL, pw);
+                break;
+            }
+        }
+    }
+
+    @Override
+    void setLogFrequency(boolean onFrame, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing log frequency to "
+                + ((onFrame) ? "frame" : "transaction"));
+        mLogOnFrame = onFrame;
+    }
+
+    @Override
+    void setBufferCapacity(int capacity, PrintWriter pw) {
+        logAndPrintln(pw, "Setting window tracing buffer capacity to " + capacity + "bytes");
+        mBuffer.setCapacity(capacity);
+    }
+
+    @Override
+    boolean isEnabled() {
+        return mEnabledLockFree;
+    }
+
+    @Override
+    int onShellCommand(ShellCommand shell) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        String cmd = shell.getNextArgRequired();
+        switch (cmd) {
+            case "start":
+                startTrace(pw);
+                return 0;
+            case "stop":
+                stopTrace(pw);
+                return 0;
+            case "save-for-bugreport":
+                saveForBugreport(pw);
+                return 0;
+            case "status":
+                logAndPrintln(pw, getStatus());
+                return 0;
+            case "frame":
+                setLogFrequency(true /* onFrame */, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            case "transaction":
+                setLogFrequency(false /* onFrame */, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            case "level":
+                String logLevelStr = shell.getNextArgRequired().toLowerCase();
+                switch (logLevelStr) {
+                    case "all": {
+                        setLogLevel(WindowTraceLogLevel.ALL, pw);
+                        break;
+                    }
+                    case "trim": {
+                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
+                        break;
+                    }
+                    case "critical": {
+                        setLogLevel(WindowTraceLogLevel.CRITICAL, pw);
+                        break;
+                    }
+                    default: {
+                        setLogLevel(WindowTraceLogLevel.TRIM, pw);
+                        break;
+                    }
+                }
+                mBuffer.resetBuffer();
+                return 0;
+            case "size":
+                setBufferCapacity(Integer.parseInt(shell.getNextArgRequired()) * 1024, pw);
+                mBuffer.resetBuffer();
+                return 0;
+            default:
+                pw.println("Unknown command: " + cmd);
+                pw.println("Window manager trace options:");
+                pw.println("  start: Start logging");
+                pw.println("  stop: Stop logging");
+                pw.println("  save-for-bugreport: Save logging data to file if it's running.");
+                pw.println("  frame: Log trace once per frame");
+                pw.println("  transaction: Log each transaction");
+                pw.println("  size: Set the maximum log size (in KB)");
+                pw.println("  status: Print trace status");
+                pw.println("  level [lvl]: Set the log level between");
+                pw.println("    lvl may be one of:");
+                pw.println("      critical: Only visible windows with reduced information");
+                pw.println("      trim: All windows with reduced");
+                pw.println("      all: All window and information");
+                return -1;
+        }
+    }
+
+    @Override
+    String getStatus() {
+        return "Status: "
+                + ((isEnabled()) ? "Enabled" : "Disabled")
+                + "\n"
+                + "Log level: "
+                + mLogLevel
+                + "\n"
+                + mBuffer.getStatus();
+    }
+
+    @Override
+    protected void startTraceInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
+            mBuffer.resetBuffer();
+            mEnabled = mEnabledLockFree = true;
+        }
+        log(WHERE_START_TRACING);
+    }
+
+    @Override
+    protected void stopTraceInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
+            mEnabled = mEnabledLockFree = false;
+
+            if (mEnabled) {
+                logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
+                throw new IllegalStateException("tracing enabled while waiting for flush.");
+            }
+            writeTraceToFileLocked();
+            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
+        }
+    }
+
+    @Override
+    protected void saveForBugreportInternal(@Nullable PrintWriter pw) {
+        synchronized (mEnabledLock) {
+            if (!mEnabled) {
+                return;
+            }
+            mEnabled = mEnabledLockFree = false;
+            logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush.");
+            writeTraceToFileLocked();
+            logAndPrintln(pw, "Trace written to " + mTraceFile + ".");
+            logAndPrintln(pw, "Start tracing to " + mTraceFile + ".");
+            mBuffer.resetBuffer();
+            mEnabled = mEnabledLockFree = true;
+        }
+    }
+
+    @Override
+    protected void log(String where) {
+        try {
+            ProtoOutputStream os = new ProtoOutputStream();
+            long token = os.start(ENTRY);
+            dumpToProto(os, mLogLevel, where, SystemClock.elapsedRealtimeNanos());
+            os.end(token);
+            mBuffer.add(os);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Exception while tracing state", e);
+        }
+    }
+
+    @Override
+    protected boolean shouldLogOnFrame() {
+        return mLogOnFrame;
+    }
+
+    @Override
+    protected boolean shouldLogOnTransaction() {
+        return !mLogOnFrame;
+    }
+
+    private void writeTraceToFileLocked() {
+        try {
+            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFileLocked");
+            ProtoOutputStream proto = new ProtoOutputStream();
+            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+            long timeOffsetNs =
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+                    - SystemClock.elapsedRealtimeNanos();
+            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
+            mBuffer.writeTraceToFile(mTraceFile, proto);
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to write buffer to file", e);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index eab7364..5719810 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -314,6 +314,7 @@
 
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
     void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override;
+    void notifyConfigurationChanged(nsecs_t when) override;
     std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay(
             const InputDeviceIdentifier& identifier,
             const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override;
@@ -331,7 +332,6 @@
 
     void notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask,
                       uint32_t policyFlags) override;
-    void notifyConfigurationChanged(nsecs_t when) override;
     // ANR-related callbacks -- start
     void notifyNoFocusedWindowAnr(const std::shared_ptr<InputApplicationHandle>& handle) override;
     void notifyWindowUnresponsive(const sp<IBinder>& token, std::optional<gui::Pid> pid,
@@ -1852,10 +1852,6 @@
                                        const std::shared_ptr<InputChannel>& inputChannel,
                                        void* data) {
     NativeInputManager* im = static_cast<NativeInputManager*>(data);
-
-    ALOGW("Input channel object '%s' was disposed without first being removed with "
-          "the input manager!",
-          inputChannel->getName().c_str());
     im->removeInputChannel(inputChannel->getConnectionToken());
 }
 
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index eeb8b9b..ec7406a 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -315,6 +315,42 @@
         <xs:element name="screenBrightnessRampDecrease" type="nonNegativeDecimal">
             <xs:annotation name="final"/>
         </xs:element>
+
+        <!-- The minimum HDR layer % at which hdr boost will be applied with transition point cap.
+             Should be lower or equal to minimumHdrPercentOfScreenForHbm. -->
+        <xs:element name="minimumHdrPercentOfScreenForNbm" type="nonNegativeDecimal"
+            minOccurs="0" maxOccurs="1">
+            <xs:annotation name="nullable"/>
+            <xs:annotation name="final"/>
+        </xs:element>
+        <!-- The minimum HDR layer size at which hdr boost will be applied. -->
+        <xs:element name="minimumHdrPercentOfScreenForHbm" type="nonNegativeDecimal"
+            minOccurs="0" maxOccurs="1">
+            <xs:annotation name="nullable"/>
+            <xs:annotation name="final"/>
+        </xs:element>
+        <!-- If hdr boost is allowed in low power mode. -->
+        <xs:element name="allowInLowPowerMode" type="xs:boolean" minOccurs="0" maxOccurs="1">
+            <xs:annotation name="nonnull"/>
+            <xs:annotation name="final"/>
+        </xs:element>
+        <!-- sdrNits, hdrRatio This LUT specifies how to boost HDR brightness at given SDR brightness (nits). -->
+        <!-- sdr brightness to hdr boost ratio map
+           Format: first = sdrNits, second = hdrRatio. E.g. :
+           <sdrHdrRatioMap>
+               <point>
+                   <first>2.0</first>   // sdrNits
+                   <second>4.0</second> // hdrRatio
+               </point>
+               ....
+           </sdrHdrRatioMap>
+        -->
+        <xs:element type="nonNegativeFloatToFloatMap" name="sdrHdrRatioMap" minOccurs="0" maxOccurs="1">
+            <xs:annotation name="nullable"/>
+            <xs:annotation name="final"/>
+        </xs:element>
+
+
     </xs:complexType>
 
     <!-- Maps to PowerManager.THERMAL_STATUS_* values. -->
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 757b23a..68d74cf 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -226,16 +226,24 @@
 
   public class HdrBrightnessConfig {
     ctor public HdrBrightnessConfig();
+    method @NonNull public final boolean getAllowInLowPowerMode();
     method public final java.math.BigInteger getBrightnessDecreaseDebounceMillis();
     method public final java.math.BigInteger getBrightnessIncreaseDebounceMillis();
     method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getBrightnessMap();
+    method @Nullable public final java.math.BigDecimal getMinimumHdrPercentOfScreenForHbm();
+    method @Nullable public final java.math.BigDecimal getMinimumHdrPercentOfScreenForNbm();
     method public final java.math.BigDecimal getScreenBrightnessRampDecrease();
     method public final java.math.BigDecimal getScreenBrightnessRampIncrease();
+    method @Nullable public final com.android.server.display.config.NonNegativeFloatToFloatMap getSdrHdrRatioMap();
+    method public final void setAllowInLowPowerMode(@NonNull boolean);
     method public final void setBrightnessDecreaseDebounceMillis(java.math.BigInteger);
     method public final void setBrightnessIncreaseDebounceMillis(java.math.BigInteger);
     method public final void setBrightnessMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap);
+    method public final void setMinimumHdrPercentOfScreenForHbm(@Nullable java.math.BigDecimal);
+    method public final void setMinimumHdrPercentOfScreenForNbm(@Nullable java.math.BigDecimal);
     method public final void setScreenBrightnessRampDecrease(java.math.BigDecimal);
     method public final void setScreenBrightnessRampIncrease(java.math.BigDecimal);
+    method public final void setSdrHdrRatioMap(@Nullable com.android.server.display.config.NonNegativeFloatToFloatMap);
   }
 
   public class HighBrightnessMode {
diff --git a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
index 6b20ef1..996daf5 100644
--- a/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
+++ b/services/permission/java/com/android/server/permission/access/util/AtomicFileExtensions.kt
@@ -47,9 +47,8 @@
 /** Write to actual file and reserve file. */
 @Throws(IOException::class)
 inline fun AtomicFile.writeWithReserveCopy(block: (FileOutputStream) -> Unit) {
-    val reserveFile = File(baseFile.parentFile, baseFile.name + ".reservecopy")
-    reserveFile.delete()
     writeInlined(block)
+    val reserveFile = File(baseFile.parentFile, baseFile.name + ".reservecopy")
     try {
         FileInputStream(baseFile).use { inputStream ->
             FileOutputStream(reserveFile).use { outputStream ->
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index 6a86379..bacde10 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -25,6 +25,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.notNull;
@@ -38,6 +39,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.hardware.input.IInputManager;
@@ -157,6 +159,7 @@
                 mockitoSession()
                         .initMocks(this)
                         .strictness(Strictness.LENIENT)
+                        .spyStatic(InputMethodUtils.class)
                         .mockStatic(ServiceManager.class)
                         .mockStatic(SystemServerInitThreadPool.class)
                         .startMocking();
@@ -227,6 +230,10 @@
                 .thenReturn(TEST_IME_TARGET_INFO);
         when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder);
 
+        // This changes the real IME component state. Not appropriate to do in tests.
+        doNothing().when(() -> InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
+                        any(PackageManager.class), anyList()));
+
         // Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(),
         // which is ok to be mocked out for now.
         doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString()));
@@ -243,9 +250,13 @@
                         Process.THREAD_PRIORITY_FOREGROUND,
                         true /* allowIo */);
         mIoThread.start();
+
+        final var ioHandler = spy(Handler.createAsync(mIoThread.getLooper()));
+        doReturn(true).when(ioHandler).post(any());
+
         mInputMethodManagerService = new InputMethodManagerService(mContext,
                 InputMethodManagerService.shouldEnableConcurrentMultiUserMode(mContext),
-                mServiceThread.getLooper(), Handler.createAsync(mIoThread.getLooper()),
+                mServiceThread.getLooper(), ioHandler,
                 unusedUserId -> mMockInputMethodBindingController);
         spyOn(mInputMethodManagerService);
 
@@ -257,16 +268,9 @@
         // Public local InputMethodManagerService.
         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
         lifecycle.onStart();
-        try {
-            // After this boot phase, services can broadcast Intents.
-            lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
-        } catch (SecurityException e) {
-            // Security exception to permission denial is expected in test, mocking out to ensure
-            // InputMethodManagerService as system ready state.
-            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
-                throw e;
-            }
-        }
+
+        // After this boot phase, services can broadcast Intents.
+        lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
 
         // Call InputMethodManagerService#addClient() as a preparation to start interacting with it.
         mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
index 9ca4f1d..81fb1a0 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
@@ -18,23 +18,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.content.pm.UserInfo;
-import android.os.ConditionVariable;
-import android.os.Handler;
-import android.os.Looper;
 import android.platform.test.ravenwood.RavenwoodRule;
 
-import com.android.server.pm.UserManagerInternal;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -52,13 +41,8 @@
             .setProvideMainThread(true).build();
 
     @Mock
-    private UserManagerInternal mMockUserManagerInternal;
-
-    @Mock
     private InputMethodManagerService mMockInputMethodManagerService;
 
-    private Handler mHandler;
-
     private IntFunction<InputMethodBindingController> mBindingControllerFactory;
 
     @Before
@@ -66,7 +50,6 @@
         MockitoAnnotations.initMocks(this);
         SecureSettingsWrapper.startTestMode();
 
-        mHandler = new Handler(Looper.getMainLooper());
         mBindingControllerFactory = new IntFunction<InputMethodBindingController>() {
 
             @Override
@@ -81,51 +64,20 @@
         SecureSettingsWrapper.endTestMode();
     }
 
-    @Test
-    public void testUserDataRepository_addsNewUserInfoOnUserCreatedEvent() {
-        // Create UserDataRepository and capture the user lifecycle listener
-        final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
-        final var bindingControllerFactorySpy = spy(mBindingControllerFactory);
-        final var repository = new UserDataRepository(mHandler,
-                mMockUserManagerInternal, bindingControllerFactorySpy);
-
-        verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
-        final var listener = captor.getValue();
-
-        // Assert that UserDataRepository is empty and then call onUserCreated
-        assertThat(collectUserData(repository)).isEmpty();
-        final var userInfo = new UserInfo();
-        userInfo.id = ANY_USER_ID;
-        listener.onUserCreated(userInfo, /* unused token */ new Object());
-        waitForIdle();
-
-        // Assert UserDataRepository remains to be empty.
-        assertThat(collectUserData(repository)).isEmpty();
-    }
-
+    // TODO(b/352615651): Move this to end-to-end test.
     @Test
     public void testUserDataRepository_removesUserInfoOnUserRemovedEvent() {
-        // Create UserDataRepository and capture the user lifecycle listener
-        final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
-        final var repository = new UserDataRepository(mHandler,
-                mMockUserManagerInternal,
+        // Create UserDataRepository
+        final var repository = new UserDataRepository(
                 userId -> new InputMethodBindingController(userId, mMockInputMethodManagerService));
 
-        verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
-        final var listener = captor.getValue();
-
         // Add one UserData ...
-        synchronized (ImfLock.class) {
-            final var userData = repository.getOrCreate(ANY_USER_ID);
-            assertThat(userData.mUserId).isEqualTo(ANY_USER_ID);
-        }
+        final var userData = repository.getOrCreate(ANY_USER_ID);
+        assertThat(userData.mUserId).isEqualTo(ANY_USER_ID);
 
         // ... and then call onUserRemoved
         assertThat(collectUserData(repository)).hasSize(1);
-        final var userInfo = new UserInfo();
-        userInfo.id = ANY_USER_ID;
-        listener.onUserRemoved(userInfo);
-        waitForIdle();
+        repository.remove(ANY_USER_ID);
 
         // Assert UserDataRepository is now empty
         assertThat(collectUserData(repository)).isEmpty();
@@ -133,13 +85,10 @@
 
     @Test
     public void testGetOrCreate() {
-        final var repository = new UserDataRepository(mHandler,
-                mMockUserManagerInternal, mBindingControllerFactory);
+        final var repository = new UserDataRepository(mBindingControllerFactory);
 
-        synchronized (ImfLock.class) {
-            final var userData = repository.getOrCreate(ANY_USER_ID);
-            assertThat(userData.mUserId).isEqualTo(ANY_USER_ID);
-        }
+        final var userData = repository.getOrCreate(ANY_USER_ID);
+        assertThat(userData.mUserId).isEqualTo(ANY_USER_ID);
 
         final var allUserData = collectUserData(repository);
         assertThat(allUserData).hasSize(1);
@@ -151,15 +100,8 @@
 
     private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
         final var collected = new ArrayList<UserDataRepository.UserData>();
-        synchronized (ImfLock.class) {
-            repository.forAllUserData(userData -> collected.add(userData));
-        }
+        repository.forAllUserData(userData -> collected.add(userData));
         return collected;
     }
 
-    private void waitForIdle() {
-        final var done = new ConditionVariable();
-        mHandler.post(done::open);
-        done.block();
-    }
 }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 89b4aea..7138306 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -1027,6 +1027,25 @@
     }
 
     @Test
+    public void testWriteReadDebuggable() {
+        Settings settings = makeSettings();
+        PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
+        packageSetting.setAppId(Process.FIRST_APPLICATION_UID);
+        packageSetting.setPkg(PackageImpl.forTesting(PACKAGE_NAME_1).hideAsParsed()
+                .setUid(packageSetting.getAppId())
+                .hideAsFinal());
+
+        packageSetting.setDebuggable(true);
+        settings.mPackages.put(PACKAGE_NAME_1, packageSetting);
+
+        settings.writeLPr(computer, /* sync= */ true);
+        settings.mPackages.clear();
+
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        assertThat(settings.getPackageLPr(PACKAGE_NAME_1).isDebuggable(), is(true));
+    }
+
+    @Test
     public void testWriteReadArchiveState() {
         Settings settings = makeSettings();
         PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 9a25b1a..3437923 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -54,6 +54,7 @@
 
 import com.android.internal.R;
 import com.android.server.display.config.HdrBrightnessData;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.config.HysteresisLevels;
 import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
 import com.android.server.display.config.RefreshRateData;
@@ -436,7 +437,7 @@
     public void testHighBrightnessModeDataFromDisplayConfig() throws IOException {
         setupDisplayDeviceConfigFromDisplayConfigFile();
 
-        DisplayDeviceConfig.HighBrightnessModeData hbmData =
+        HighBrightnessModeData hbmData =
                 mDisplayDeviceConfig.getHighBrightnessModeData();
         assertNotNull(hbmData);
         assertEquals(BRIGHTNESS[1], hbmData.transitionPoint, ZERO_DELTA);
@@ -671,14 +672,14 @@
         HdrBrightnessData data = mDisplayDeviceConfig.getHdrBrightnessData();
 
         assertNotNull(data);
-        assertEquals(2, data.mMaxBrightnessLimits.size());
-        assertEquals(13000, data.mBrightnessDecreaseDebounceMillis);
-        assertEquals(0.1f, data.mScreenBrightnessRampDecrease, SMALL_DELTA);
-        assertEquals(1000, data.mBrightnessIncreaseDebounceMillis);
-        assertEquals(0.11f, data.mScreenBrightnessRampIncrease, SMALL_DELTA);
+        assertEquals(2, data.maxBrightnessLimits.size());
+        assertEquals(13000, data.brightnessDecreaseDebounceMillis);
+        assertEquals(0.1f, data.screenBrightnessRampDecrease, SMALL_DELTA);
+        assertEquals(1000, data.brightnessIncreaseDebounceMillis);
+        assertEquals(0.11f, data.screenBrightnessRampIncrease, SMALL_DELTA);
 
-        assertEquals(0.3f, data.mMaxBrightnessLimits.get(500f), SMALL_DELTA);
-        assertEquals(0.6f, data.mMaxBrightnessLimits.get(1200f), SMALL_DELTA);
+        assertEquals(0.3f, data.maxBrightnessLimits.get(500f), SMALL_DELTA);
+        assertEquals(0.6f, data.maxBrightnessLimits.get(1200f), SMALL_DELTA);
     }
 
     private void verifyConfigValuesFromConfigResource() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 2018e1a..5c29156 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -83,6 +83,7 @@
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.config.HysteresisLevels;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -2425,7 +2426,7 @@
         @Override
         HighBrightnessModeController getHighBrightnessModeController(Handler handler, int width,
                 int height, IBinder displayToken, String displayUniqueId, float brightnessMin,
-                float brightnessMax, DisplayDeviceConfig.HighBrightnessModeData hbmData,
+                float brightnessMax, HighBrightnessModeData hbmData,
                 HighBrightnessModeController.HdrBrightnessDeviceConfig hdrBrightnessCfg,
                 Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata,
                 Context context) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java
index 8e01a11..cde87b9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeControllerTest.java
@@ -23,7 +23,6 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE;
-import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
 import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID;
 
 import static org.junit.Assert.assertEquals;
@@ -56,8 +55,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
-import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
 import com.android.server.display.HighBrightnessModeController.Injector;
+import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.Before;
@@ -77,6 +76,7 @@
     private static final long TIME_ALLOWED_IN_WINDOW_MILLIS = 12 * 1000;
     private static final long TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS = 5 * 1000;
     private static final boolean ALLOW_IN_LOW_POWER_MODE = false;
+    private static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f;
 
     private static final float DEFAULT_MIN = 0.01f;
     private static final float DEFAULT_MAX = 0.80f;
@@ -103,7 +103,8 @@
     private static final HighBrightnessModeData DEFAULT_HBM_DATA =
             new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS,
                     TIME_ALLOWED_IN_WINDOW_MILLIS, TIME_MINIMUM_AVAILABLE_TO_ENABLE_MILLIS,
-                    ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT);
+                    ALLOW_IN_LOW_POWER_MODE, HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT,
+                    null, null, true);
 
     @Before
     public void setUp() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
index 7e7ccf7..7132bc1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
@@ -22,6 +22,8 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
+import com.android.server.display.config.HighBrightnessModeData;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -39,7 +41,7 @@
     private DisplayDeviceConfig mDdcMock;
 
     @Mock
-    private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock;
+    private HighBrightnessModeData mHbmDataMock;
 
     private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper;
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 6d138c5..1729ad5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.display;
 
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.DEFAULT_DISPLAY_GROUP;
 import static android.view.Display.FLAG_REAR;
@@ -62,9 +62,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.NonNull;
 import android.app.PropertyInvalidatedCache;
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.devicestate.DeviceState;
 import android.os.Handler;
 import android.os.IPowerManager;
 import android.os.IThermalService;
@@ -103,7 +105,9 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -111,9 +115,12 @@
     private static int sUniqueTestDisplayId = 0;
     private static final int TIMEOUT_STATE_TRANSITION_MILLIS = 500;
     private static final int FOLD_SETTLE_DELAY = 1000;
-    private static final int DEVICE_STATE_CLOSED = 0;
-    private static final int DEVICE_STATE_HALF_OPEN = 1;
-    private static final int DEVICE_STATE_OPEN = 2;
+    private static final DeviceState DEVICE_STATE_CLOSED = createDeviceState(0, "Zero",
+            Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP), Collections.emptySet());
+    private static final DeviceState DEVICE_STATE_HALF_OPEN = createDeviceState(1, "One",
+            Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
+    private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two",
+            Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
     private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0;
     private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2;
     private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
@@ -703,8 +710,7 @@
                 /* isInteractive= */true,
                 /* isBootCompleted= */true));
         assertFalse(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
-                INVALID_DEVICE_STATE_IDENTIFIER,
-                /* isInteractive= */true,
+                INVALID_DEVICE_STATE /* currentState */, /* isInteractive= */true,
                 /* isBootCompleted= */true));
     }
 
@@ -932,7 +938,7 @@
         // We can only have one default display
         assertEquals(DEFAULT_DISPLAY, id(display1));
 
-        mLogicalDisplayMapper.setDeviceStateLocked(0);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED);
         advanceTime(1000);
         // The new state is not applied until the boot is completed
         assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
@@ -953,7 +959,7 @@
         assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device2)
                 .getDisplayInfoLocked().thermalBrightnessThrottlingDataId);
 
-        mLogicalDisplayMapper.setDeviceStateLocked(1);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_HALF_OPEN);
         advanceTime(1000);
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
         assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
@@ -966,7 +972,7 @@
                 mLogicalDisplayMapper.getDisplayLocked(device2)
                         .getDisplayInfoLocked().thermalBrightnessThrottlingDataId);
 
-        mLogicalDisplayMapper.setDeviceStateLocked(2);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_OPEN);
         advanceTime(1000);
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked());
         assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
@@ -1043,7 +1049,7 @@
         // 3) Send DISPLAY_DEVICE_EVENT_CHANGE to inform the mapper of the new display state
         // 4) Dispatch handler events.
         mLogicalDisplayMapper.onBootCompleted();
-        mLogicalDisplayMapper.setDeviceStateLocked(0);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED);
         mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED);
         advanceTime(1000);
         final int[] allDisplayIds = mLogicalDisplayMapper.getDisplayIdsLocked(
@@ -1073,7 +1079,7 @@
                 /* includeDisabled= */ false));
 
         // Now do it again to go back to state 1
-        mLogicalDisplayMapper.setDeviceStateLocked(1);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_HALF_OPEN);
         mDisplayDeviceRepo.onDisplayDeviceEvent(device3, DISPLAY_DEVICE_EVENT_CHANGED);
         advanceTime(1000);
         final int[] threeDisplaysEnabled = mLogicalDisplayMapper.getDisplayIdsLocked(
@@ -1127,7 +1133,7 @@
         // We can only have one default display
         assertEquals(DEFAULT_DISPLAY, id(display1));
 
-        mLogicalDisplayMapper.setDeviceStateLocked(0);
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED);
         advanceTime(1000);
         mLogicalDisplayMapper.onBootCompleted();
         advanceTime(1000);
@@ -1180,13 +1186,15 @@
         Layout layout = new Layout();
         createDefaultDisplay(layout, outer);
         createNonDefaultDisplay(layout, inner, /* enabled= */ false, /* group= */ null);
-        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_CLOSED)).thenReturn(layout);
+        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_CLOSED.getIdentifier())).thenReturn(
+                layout);
 
         layout = new Layout();
         createNonDefaultDisplay(layout, outer, /* enabled= */ false, /* group= */ null);
         createDefaultDisplay(layout, inner);
-        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_HALF_OPEN)).thenReturn(layout);
-        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_OPEN)).thenReturn(layout);
+        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_HALF_OPEN.getIdentifier())).thenReturn(
+                layout);
+        when(mDeviceStateToLayoutMapSpy.get(DEVICE_STATE_OPEN.getIdentifier())).thenReturn(layout);
         when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4);
 
         add(outer);
@@ -1317,6 +1325,15 @@
         assertNotEquals(DEFAULT_DISPLAY, id(displayRemoved));
     }
 
+    private static DeviceState createDeviceState(int identifier, @NonNull String name,
+            @NonNull Set<@DeviceState.SystemDeviceStateProperties Integer> systemProperties,
+            @NonNull Set<@DeviceState.PhysicalDeviceStateProperties Integer> physicalProperties) {
+        DeviceState.Configuration deviceStateConfiguration = new DeviceState.Configuration.Builder(
+                identifier, name).setSystemProperties(systemProperties).setPhysicalProperties(
+                physicalProperties).build();
+        return new DeviceState(deviceStateConfiguration);
+    }
+
     private final static class FoldableDisplayDevices {
         final TestDisplayDevice mOuter;
         final TestDisplayDevice mInner;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index c785ea6..7212856 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -34,6 +34,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.display.AutomaticBrightnessController;
+import com.android.server.display.config.DisplayDeviceConfigTestUtilsKt;
 import com.android.server.display.config.HdrBrightnessData;
 import com.android.server.testutils.OffsettableClock;
 import com.android.server.testutils.TestHandler;
@@ -54,13 +55,14 @@
     private static final float FLOAT_TOLERANCE = 0.0001f;
     private static final long SEND_TIME_TOLERANCE = 100;
 
-    private static final HdrBrightnessData TEST_HDR_DATA = new HdrBrightnessData(
-            Map.of(500f, 0.6f),
-            /* brightnessIncreaseDebounceMillis= */ 1000,
-            /* screenBrightnessRampIncrease= */ 0.02f,
-            /* brightnessDecreaseDebounceMillis= */ 3000,
-            /* screenBrightnessRampDecrease= */0.04f
-    );
+    private static final HdrBrightnessData TEST_HDR_DATA = DisplayDeviceConfigTestUtilsKt
+            .createHdrBrightnessData(
+                    Map.of(500f, 0.6f),
+                    /* brightnessIncreaseDebounceMillis= */ 1000,
+                    /* screenBrightnessRampIncrease= */ 0.02f,
+                    /* brightnessDecreaseDebounceMillis= */ 3000,
+                    /* screenBrightnessRampDecrease= */0.04f
+            );
 
     private static final int WIDTH = 600;
     private static final int HEIGHT = 800;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt
new file mode 100644
index 0000000..3b3d6f7
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 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.display.config
+
+import android.util.Spline
+import android.util.Xml
+import com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.OutputStreamWriter
+import org.xmlpull.v1.XmlSerializer
+
+fun createRefreshRateData(
+    defaultRefreshRate: Int = 60,
+    defaultPeakRefreshRate: Int = 60,
+    defaultRefreshRateInHbmHdr: Int = 60,
+    defaultRefreshRateInHbmSunlight: Int = 60,
+    lowPowerSupportedModes: List<SupportedModeData> = emptyList(),
+    lowLightBlockingZoneSupportedModes: List<SupportedModeData> = emptyList()
+): RefreshRateData {
+    return RefreshRateData(
+        defaultRefreshRate, defaultPeakRefreshRate,
+        defaultRefreshRateInHbmHdr, defaultRefreshRateInHbmSunlight,
+        lowPowerSupportedModes, lowLightBlockingZoneSupportedModes
+    )
+}
+
+@JvmOverloads
+fun createHdrBrightnessData(
+    maxBrightnessLimits: Map<Float, Float> = mapOf(Pair(500f, 0.6f)),
+    brightnessIncreaseDebounceMillis: Long = 1000,
+    screenBrightnessRampIncrease: Float = 0.02f,
+    brightnessDecreaseDebounceMillis: Long = 3000,
+    screenBrightnessRampDecrease: Float = 0.04f,
+    minimumHdrPercentOfScreenForNbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT,
+    minimumHdrPercentOfScreenForHbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT,
+    allowInLowPowerMode: Boolean = false,
+    sdrToHdrRatioSpline: Spline? = null
+): HdrBrightnessData {
+    return HdrBrightnessData(
+        maxBrightnessLimits,
+        brightnessIncreaseDebounceMillis,
+        screenBrightnessRampIncrease,
+        brightnessDecreaseDebounceMillis,
+        screenBrightnessRampDecrease,
+        minimumHdrPercentOfScreenForNbm,
+        minimumHdrPercentOfScreenForHbm,
+        allowInLowPowerMode,
+        sdrToHdrRatioSpline
+    )
+}
+
+fun XmlSerializer.highBrightnessMode(
+    enabled: String = "true",
+    transitionPoint: String = "0.67",
+    minimumLux: String = "2500",
+    timeWindowSecs: String = "200",
+    timeMaxSecs: String = "30",
+    timeMinSecs: String = "3",
+    refreshRateRange: Pair<String, String>? = null,
+    allowInLowPowerMode: String? = null,
+    minimumHdrPercentOfScreen: String? = null,
+    sdrHdrRatioMap: List<Pair<String, String>>? = null,
+) {
+    element("highBrightnessMode") {
+        attribute("", "enabled", enabled)
+        element("transitionPoint", transitionPoint)
+        element("minimumLux", minimumLux)
+        element("timing") {
+            element("timeWindowSecs", timeWindowSecs)
+            element("timeMaxSecs", timeMaxSecs)
+            element("timeMinSecs", timeMinSecs)
+        }
+        pair("refreshRate", "minimum", "maximum", refreshRateRange)
+        element("allowInLowPowerMode", allowInLowPowerMode)
+        element("minimumHdrPercentOfScreen", minimumHdrPercentOfScreen)
+        map("sdrHdrRatioMap", "point", "sdrNits", "hdrRatio", sdrHdrRatioMap)
+    }
+}
+
+fun XmlSerializer.hdrBrightnessConfig(
+    brightnessMap: List<Pair<String, String>> = listOf(Pair("500", "0.6")),
+    brightnessIncreaseDebounceMillis: String = "1000",
+    screenBrightnessRampIncrease: String = "0.02",
+    brightnessDecreaseDebounceMillis: String = "3000",
+    screenBrightnessRampDecrease: String = "0.04",
+    minimumHdrPercentOfScreenForNbm: String? = null,
+    minimumHdrPercentOfScreenForHbm: String? = null,
+    allowInLowPowerMode: String? = null,
+    sdrHdrRatioMap: List<Pair<String, String>>? = null,
+) {
+    element("hdrBrightnessConfig") {
+        map("brightnessMap", "point", "first", "second", brightnessMap)
+        element("brightnessIncreaseDebounceMillis", brightnessIncreaseDebounceMillis)
+        element("screenBrightnessRampIncrease", screenBrightnessRampIncrease)
+        element("brightnessDecreaseDebounceMillis", brightnessDecreaseDebounceMillis)
+        element("screenBrightnessRampDecrease", screenBrightnessRampDecrease)
+        element("minimumHdrPercentOfScreenForNbm", minimumHdrPercentOfScreenForNbm)
+        element("minimumHdrPercentOfScreenForHbm", minimumHdrPercentOfScreenForHbm)
+        element("allowInLowPowerMode", allowInLowPowerMode)
+        map("sdrHdrRatioMap", "point", "first", "second", sdrHdrRatioMap)
+    }
+}
+
+fun createDisplayConfiguration(content: XmlSerializer.() -> Unit = { }): DisplayConfiguration {
+    val byteArrayOutputStream = ByteArrayOutputStream()
+    val xmlSerializer = Xml.newSerializer()
+    OutputStreamWriter(byteArrayOutputStream).use { writer ->
+        xmlSerializer.setOutput(writer)
+        xmlSerializer.startDocument("UTF-8", true)
+        xmlSerializer.startTag("", "displayConfiguration")
+        xmlSerializer.content()
+        xmlSerializer.endTag("", "displayConfiguration")
+        xmlSerializer.endDocument()
+    }
+    return XmlParser.read(ByteArrayInputStream(byteArrayOutputStream.toByteArray()))
+}
+
+private fun XmlSerializer.map(
+    rootName: String,
+    nodeName: String,
+    keyName: String,
+    valueName: String,
+    map: List<Pair<String, String>>?
+) {
+    map?.let { m ->
+        element(rootName) {
+            m.forEach { e -> pair(nodeName, keyName, valueName, e) }
+        }
+    }
+}
+
+private fun XmlSerializer.pair(
+    nodeName: String,
+    keyName: String,
+    valueName: String,
+    pair: Pair<String, String>?
+) {
+    pair?.let {
+        element(nodeName) {
+            element(keyName, pair.first)
+            element(valueName, pair.second)
+        }
+    }
+}
+
+private fun XmlSerializer.element(name: String, content: String?) {
+    if (content != null) {
+        startTag("", name)
+        text(content)
+        endTag("", name)
+    }
+}
+
+private fun XmlSerializer.element(name: String, content: XmlSerializer.() -> Unit) {
+    startTag("", name)
+    content()
+    endTag("", name)
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt
new file mode 100644
index 0000000..19c6924
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 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.display.config
+
+import android.util.Spline.createSpline
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayBrightnessState
+import com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class HdrBrightnessDataTest {
+
+    @Test
+    fun `test HdrBrightnessData default configuration`() {
+        val displayConfiguration = createDisplayConfiguration {
+            hdrBrightnessConfig(
+                brightnessDecreaseDebounceMillis = "3000",
+                screenBrightnessRampDecrease = "0.05",
+                brightnessIncreaseDebounceMillis = "2000",
+                screenBrightnessRampIncrease = "0.03",
+                brightnessMap = listOf(Pair("500", "0.6"), Pair("600", "0.7")),
+                minimumHdrPercentOfScreenForNbm = null,
+                minimumHdrPercentOfScreenForHbm = null,
+                allowInLowPowerMode = null,
+                sdrHdrRatioMap = null,
+            )
+        }
+
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        assertThat(hdrBrightnessData).isNotNull()
+
+        assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(3000)
+        assertThat(hdrBrightnessData.screenBrightnessRampDecrease).isEqualTo(0.05f)
+        assertThat(hdrBrightnessData.brightnessIncreaseDebounceMillis).isEqualTo(2000)
+        assertThat(hdrBrightnessData.screenBrightnessRampIncrease).isEqualTo(0.03f)
+
+        assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(2)
+        assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(500f, 0.6f)
+        assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(600f, 0.7f)
+
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(
+            HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT
+        )
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(
+            HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT
+        )
+        assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse()
+        assertThat(hdrBrightnessData.sdrToHdrRatioSpline).isNull()
+    }
+
+    @Test
+    fun `test HdrBrightnessData fallback configuration`() {
+        val displayConfiguration = createDisplayConfiguration {
+            hdrBrightnessConfig(
+                minimumHdrPercentOfScreenForNbm = null,
+                minimumHdrPercentOfScreenForHbm = null,
+                allowInLowPowerMode = null,
+                sdrHdrRatioMap = null,
+            )
+            highBrightnessMode(
+                minimumHdrPercentOfScreen = "0.2",
+                sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0"))
+            )
+        }
+
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        assertThat(hdrBrightnessData).isNotNull()
+
+        assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f)
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f)
+        assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse()
+
+        val expectedSpline = createSpline(floatArrayOf(2.0f, 5.0f), floatArrayOf(4.0f, 8.0f))
+        assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString())
+            .isEqualTo(expectedSpline.toString())
+    }
+
+    @Test
+    fun `test HdrBrightnessData fallback configuration no hdrBrightnessConfig`() {
+        val displayConfiguration = createDisplayConfiguration {
+            highBrightnessMode(
+                minimumHdrPercentOfScreen = "0.2",
+                sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0"))
+            )
+        }
+
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        assertThat(hdrBrightnessData).isNotNull()
+
+        assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(0)
+        assertThat(hdrBrightnessData.screenBrightnessRampDecrease)
+            .isEqualTo(DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET)
+        assertThat(hdrBrightnessData.brightnessIncreaseDebounceMillis).isEqualTo(0)
+        assertThat(hdrBrightnessData.screenBrightnessRampIncrease)
+            .isEqualTo(DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET)
+
+        assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(0)
+
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f)
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f)
+        assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse()
+
+        val expectedSpline = createSpline(floatArrayOf(2.0f, 5.0f), floatArrayOf(4.0f, 8.0f))
+        assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString())
+            .isEqualTo(expectedSpline.toString())
+    }
+
+    @Test
+    fun `test HdrBrightnessData configuration no configuration`() {
+        val displayConfiguration = createDisplayConfiguration()
+
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        assertThat(hdrBrightnessData).isNull()
+    }
+
+    @Test
+    fun `test HdrBrightnessData real configuration`() {
+        val displayConfiguration = createDisplayConfiguration {
+            hdrBrightnessConfig(
+                minimumHdrPercentOfScreenForNbm = "0.3",
+                minimumHdrPercentOfScreenForHbm = "0.6",
+                allowInLowPowerMode = "true",
+                sdrHdrRatioMap = listOf(Pair("3.0", "5.0"), Pair("6.0", "8.0"))
+            )
+            highBrightnessMode(
+                minimumHdrPercentOfScreen = "0.2",
+                sdrHdrRatioMap = listOf(Pair("2.0", "4.0"), Pair("5.0", "8.0"))
+            )
+        }
+
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        assertThat(hdrBrightnessData).isNotNull()
+
+        assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f)
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.6f)
+        assertThat(hdrBrightnessData.allowInLowPowerMode).isTrue()
+
+        val expectedSpline = createSpline(floatArrayOf(3.0f, 6.0f), floatArrayOf(5.0f, 8.0f))
+        assertThat(hdrBrightnessData.sdrToHdrRatioSpline.toString())
+            .isEqualTo(expectedSpline.toString())
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
index 95702aa..3c77ec9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
@@ -25,6 +25,7 @@
 import com.android.server.display.DisplayDeviceConfig
 import com.android.server.display.config.RefreshRateData
 import com.android.server.display.config.SupportedModeData
+import com.android.server.display.config.createRefreshRateData
 import com.android.server.display.feature.DisplayManagerFlags
 import com.android.server.display.mode.DisplayModeDirector.DisplayDeviceConfigProvider
 import com.android.server.testutils.TestHandler
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt
index e431c8c..4fc574a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt
@@ -29,6 +29,7 @@
 import com.android.server.display.DisplayDeviceConfig
 import com.android.server.display.config.RefreshRateData
 import com.android.server.display.config.SupportedModeData
+import com.android.server.display.config.createRefreshRateData
 import com.android.server.display.feature.DisplayManagerFlags
 import com.android.server.display.mode.DisplayModeDirector.DisplayDeviceConfigProvider
 import com.android.server.display.mode.SupportedRefreshRatesVote.RefreshRates
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt
index 5b07166..0b34fce 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/TestUtils.kt
@@ -16,9 +16,6 @@
 
 package com.android.server.display.mode
 
-import com.android.server.display.config.RefreshRateData
-import com.android.server.display.config.SupportedModeData
-
 internal fun createVotesSummary(
         isDisplayResolutionRangeVotingEnabled: Boolean = true,
         supportedModesVoteEnabled: Boolean = true,
@@ -29,15 +26,3 @@
             loggingEnabled, supportsFrameRateOverride)
 }
 
-fun createRefreshRateData(
-        defaultRefreshRate: Int = 60,
-        defaultPeakRefreshRate: Int = 60,
-        defaultRefreshRateInHbmHdr: Int = 60,
-        defaultRefreshRateInHbmSunlight: Int = 60,
-        lowPowerSupportedModes: List<SupportedModeData> = emptyList(),
-        lowLightBlockingZoneSupportedModes: List<SupportedModeData> = emptyList()
-): RefreshRateData {
-        return RefreshRateData(defaultRefreshRate, defaultPeakRefreshRate,
-                defaultRefreshRateInHbmHdr, defaultRefreshRateInHbmSunlight,
-                lowPowerSupportedModes, lowLightBlockingZoneSupportedModes)
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index 419bcb8..e610a32 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -527,7 +527,7 @@
 
         final ProcessRecord appRec = new ProcessRecord(mAms, info, info.processName, uid);
         final ProcessStatsService tracker = mAms.mProcessStats;
-        final IApplicationThread appThread = mock(IApplicationThread.class);
+        final ApplicationThreadDeferred appThread = mock(ApplicationThreadDeferred.class);
         doReturn(mock(IBinder.class)).when(appThread).asBinder();
         appRec.makeActive(appThread, tracker);
         mAms.mProcessList.addProcessNameLocked(appRec);
@@ -701,7 +701,8 @@
         final var wpc = fifoProc.getWindowProcessController();
         spyOn(wpc);
         doReturn(true).when(wpc).useFifoUiScheduling();
-        fifoProc.makeActive(fifoProc.getThread(), mAms.mProcessStats);
+        fifoProc.makeActive(new ApplicationThreadDeferred(fifoProc.getThread()),
+                mAms.mProcessStats);
         assertTrue(fifoProc.useFifoUiScheduling());
         assertTrue(mAms.mSpecifiedFifoProcesses.contains(fifoProc));
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java
new file mode 100644
index 0000000..8f8c1ac
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationThreadDeferredTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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.am;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.IApplicationThread;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+
+/**
+ * Tests to verify that the ApplicationThreadDeferred properly defers binder calls to paused
+ * processes.
+ */
+@SmallTest
+public class ApplicationThreadDeferredTest {
+    private static final String TAG = "ApplicationThreadDeferredTest";
+
+    private void callDeferredApis(IApplicationThread thread) throws Exception {
+        thread.clearDnsCache();
+        thread.updateHttpProxy();
+        thread.updateTimeZone();
+        thread.scheduleLowMemory();
+    }
+
+    // Verify that the special APIs have been called count times.
+    private void verifyDeferredApis(IApplicationThread thread, int count) throws Exception {
+        verify(thread, times(count)).clearDnsCache();
+        verify(thread, times(count)).updateHttpProxy();
+        verify(thread, times(count)).updateTimeZone();
+        verify(thread, times(count)).scheduleLowMemory();
+    }
+
+    // Test the baseline behavior of IApplicationThread.  If this test fails, all other tests are
+    // suspect.
+    @Test
+    public void testBaseline() throws Exception {
+        IApplicationThread thread = mock(IApplicationThread.class);
+        callDeferredApis(thread);
+        verifyDeferredApis(thread, 1);
+    }
+
+    // Test the baseline behavior of IApplicationThreadDeferred.  If this test fails, all other
+    // tests are suspect.
+    @Test
+    public void testBaselineDeferred() throws Exception {
+        IApplicationThread thread = mock(ApplicationThreadDeferred.class);
+        callDeferredApis(thread);
+        verifyDeferredApis(thread, 1);
+    }
+
+    // Verify that a deferred thread behaves like a regular thread when it is not paused.
+    @Test
+    public void testDeferredUnpaused() throws Exception {
+        IApplicationThread base = mock(IApplicationThread.class);
+        ApplicationThreadDeferred thread = new ApplicationThreadDeferred(base, true);
+        callDeferredApis(thread);
+        verifyDeferredApis(base, 1);
+    }
+
+    // Verify that a paused deferred thread thread does not deliver any calls to its parent.  Then
+    // unpause the thread and verify that the collapsed calls are forwarded.
+    @Test
+    public void testDeferredPaused() throws Exception {
+        IApplicationThread base = mock(IApplicationThread.class);
+        ApplicationThreadDeferred thread = new ApplicationThreadDeferred(base, true);
+        thread.onProcessPaused();
+        callDeferredApis(thread);
+        callDeferredApis(thread);
+        verifyDeferredApis(base, 0);
+        thread.onProcessUnpaused();
+        verifyDeferredApis(base, 1);
+    }
+
+    // TODO: [b/302724778] Remove manual JNI load
+    static {
+        System.loadLibrary("mockingservicestestjni");
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index 80f7a06..93066d8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -189,7 +189,7 @@
 
     private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, boolean wedge)
             throws Exception {
-        final IApplicationThread thread = mock(IApplicationThread.class);
+        final ApplicationThreadDeferred thread = mock(ApplicationThreadDeferred.class);
         final IBinder threadBinder = new Binder();
         doReturn(threadBinder).when(thread).asBinder();
         doAnswer((invocation) -> {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 13ba1e5..3aaf2e5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -325,13 +325,13 @@
         ProcessRecord.updateProcessRecordNodes(r);
         mActiveProcesses.add(r);
 
-        final IApplicationThread thread;
+        final ApplicationThreadDeferred thread;
         if (dead) {
-            thread = mock(IApplicationThread.class, (invocation) -> {
+            thread = mock(ApplicationThreadDeferred.class, (invocation) -> {
                 throw new DeadObjectException();
             });
         } else {
-            thread = mock(IApplicationThread.class);
+            thread = mock(ApplicationThreadDeferred.class);
         }
         final IBinder threadBinder = new Binder();
         doReturn(threadBinder).when(thread).asBinder();
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java
index 240ddf5..0796f41 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/CacheOomRankerTest.java
@@ -760,7 +760,7 @@
         ProcessStatsService processStatsService = new ProcessStatsService(
                 mock(ActivityManagerService.class), new File(Environment.getDataSystemCeDirectory(),
                 "procstats"));
-        app.makeActive(mock(IApplicationThread.class), processStatsService);
+        app.makeActive(mock(ApplicationThreadDeferred.class), processStatsService);
         return app;
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 6366f24..1dbd532 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -3209,7 +3209,7 @@
             final ProcessReceiverRecord receivers = app.mReceivers;
             final ProcessProfileRecord profile = app.mProfile;
             final ProcessProviderRecord providers = app.mProviders;
-            app.makeActive(mock(IApplicationThread.class), mService.mProcessStats);
+            app.makeActive(mock(ApplicationThreadDeferred.class), mService.mProcessStats);
             app.setLastActivityTime(mLastActivityTime);
             app.setKilledByAm(mKilledByAm);
             app.setIsolatedEntryPoint(mIsolatedEntryPoint);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index 89c67d5..3572d23 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -193,7 +193,7 @@
 
     private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai)
             throws Exception {
-        final IApplicationThread thread = mock(IApplicationThread.class);
+        final ApplicationThreadDeferred thread = mock(ApplicationThreadDeferred.class);
         final IBinder threadBinder = new Binder();
         doReturn(threadBinder).when(thread).asBinder();
         doAnswer((invocation) -> {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
index 5f12677..1ff4a27 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java
@@ -529,7 +529,7 @@
     @SuppressWarnings("GuardedBy")
     private ProcessRecord addProcessRecord(int pid, int uid, int procState, int adj, int cap,
                 String packageName) {
-        final IApplicationThread appThread = mock(IApplicationThread.class);
+        final ApplicationThreadDeferred appThread = mock(ApplicationThreadDeferred.class);
         final IBinder threadBinder = mock(IBinder.class);
         final ProcessRecord app = makeProcessRecord(pid, uid, uid, null, 0,
                 procState, adj, cap, 0L, 0L, packageName, packageName, mAms);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java
index 7ec27be..3c43636 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceTimeoutTest.java
@@ -145,7 +145,7 @@
                 name,                  // processName
                 name,                  // packageName
                 mAms);
-        app.makeActive(mock(IApplicationThread.class), mAms.mProcessStats);
+        app.makeActive(mock(ApplicationThreadDeferred.class), mAms.mProcessStats);
         mProcessList.updateLruProcessLocked(app, false, null);
 
         final long now = SystemClock.uptimeMillis();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
index 362607b..b13fc53 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
@@ -355,7 +355,7 @@
         assertThat(stats.getNumBytesRx()).isEqualTo(13321);
         assertThat(stats.getNumPacketsTx()).isEqualTo(263);
         assertThat(stats.getNumBytesTx()).isEqualTo(7234);
-        assertThat(stats.getScanTimeMillis()).isEqualTo(2200);
+        assertThat(stats.getScanTimeMillis()).isEqualTo(200);
         assertThat(stats.getRxTimeMillis()).isEqualTo(6000);
         assertThat(stats.getTxTimeMillis()).isEqualTo(1000);
         assertThat(stats.getIdleTimeMillis()).isEqualTo(300);
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index bf47816..1decd36 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -151,7 +151,6 @@
 
     private HintManagerService mService;
     private ChannelConfig mConfig;
-    private ApplicationInfo mApplicationInfo;
 
     private static Answer<Long> fakeCreateWithConfig(Long ptr, Long sessionId) {
         return new Answer<Long>() {
@@ -168,12 +167,12 @@
         mConfig = new ChannelConfig();
         mConfig.readFlagBitmask = 1;
         mConfig.writeFlagBitmask = 2;
-        mApplicationInfo = new ApplicationInfo();
-        mApplicationInfo.category = ApplicationInfo.CATEGORY_GAME;
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.category = ApplicationInfo.CATEGORY_GAME;
         when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockPackageManager.getNameForUid(anyInt())).thenReturn(TEST_APP_NAME);
         when(mMockPackageManager.getApplicationInfo(eq(TEST_APP_NAME), anyInt()))
-                .thenReturn(mApplicationInfo);
+                .thenReturn(applicationInfo);
         when(mNativeWrapperMock.halGetHintSessionPreferredRate())
                 .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 28a5db9..e06d939 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -223,6 +223,10 @@
         Settings.System.putInt(getContext().getContentResolver(),
                 Settings.System.NOTIFICATION_LIGHT_PULSE, 1);
 
+        // Enable notification cooldown independent of device Settings
+        Settings.System.putInt(getContext().getContentResolver(),
+                Settings.System.NOTIFICATION_COOLDOWN_ENABLED, 1);
+
         Resources resources = spy(getContext().getResources());
         when(resources.getBoolean(R.bool.config_useAttentionLight)).thenReturn(true);
         when(resources.getBoolean(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
index ff1308c..1c8cb8f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeEventLoggerFake.java
@@ -137,4 +137,9 @@
         checkInRange(i);
         return mChanges.get(i).getActiveRuleTypes();
     }
+
+    public int getChangeOrigin(int i) throws IllegalArgumentException {
+        checkInRange(i);
+        return mChanges.get(i).getChangeOrigin();
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 7bb633e..4bbbc2b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -818,7 +818,7 @@
         // 1. Current ringer is normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
         // Set zen to priority-only with all notification sounds muted (so ringer will be muted)
-        Policy totalSilence = new Policy(0,0,0);
+        Policy totalSilence = new Policy(0, 0, 0);
         mZenModeHelper.setNotificationPolicy(totalSilence, UPDATE_ORIGIN_APP, 1);
         mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 
@@ -873,7 +873,7 @@
 
         // even when ringer is muted (since all ringer sounds cannot bypass DND),
         // system stream is still affected by ringer mode
-        mZenModeHelper.setNotificationPolicy(new Policy(0,0,0), UPDATE_ORIGIN_APP, 1);
+        mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0), UPDATE_ORIGIN_APP, 1);
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY,
                 UPDATE_ORIGIN_APP, "test", "caller", 1);
         ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerNotMuted =
@@ -1065,9 +1065,10 @@
     @Test
     public void testParcelConfig() {
         mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS
-                | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS
-                | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED,
-                PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE), UPDATE_ORIGIN_UNKNOWN,
+                        | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS
+                        | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED,
+                        PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE),
+                UPDATE_ORIGIN_UNKNOWN,
                 1);
         mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
                 .setShouldDimWallpaper(true)
@@ -1085,13 +1086,14 @@
     @Test
     public void testWriteXml() throws Exception {
         mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS
-                | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS
-                | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED,
-                PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, CONVERSATION_SENDERS_ANYONE),
+                        | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS
+                        | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED,
+                        PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE,
+                        CONVERSATION_SENDERS_ANYONE),
                 UPDATE_ORIGIN_UNKNOWN, 1);
         mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
-                        .setShouldDimWallpaper(true)
-                        .setShouldDisplayGrayscale(true)
+                .setShouldDimWallpaper(true)
+                .setShouldDisplayGrayscale(true)
                 .build(), UPDATE_ORIGIN_UNKNOWN, "test", 1);
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY,
                 UPDATE_ORIGIN_UNKNOWN, "test", "me", 1);
@@ -2210,7 +2212,7 @@
         customDefaultRule.name = "Schedule Default Rule";
         customDefaultRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
         ScheduleInfo scheduleInfo = new ScheduleInfo();
-        scheduleInfo.days = new int[] { Calendar.SUNDAY };
+        scheduleInfo.days = new int[]{Calendar.SUNDAY};
         scheduleInfo.startHour = 18;
         scheduleInfo.endHour = 19;
         customDefaultRule.conditionId = ZenModeConfig.toScheduleConditionId(scheduleInfo);
@@ -3027,7 +3029,7 @@
         // Turn zen mode on (to important_interruptions)
         // Need to additionally call the looper in order to finish the post-apply-config process
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null,
+                Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null,
                 Process.SYSTEM_UID);
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
@@ -3060,6 +3062,9 @@
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
+        // change origin should be populated only under modes_ui
+        assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo(
+                (Flags.modesApi() && Flags.modesUi()) ? UPDATE_ORIGIN_USER : 0);
 
         // and from turning zen mode off:
         //   - event ID: DND_TURNED_OFF
@@ -3082,6 +3087,8 @@
         } else {
             checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
         }
+        assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : 0);
     }
 
     @Test
@@ -3098,17 +3105,21 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+                UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID);
 
         // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE
+        // Note that pre-modes_ui, this event serves as a test that automatic changes to an app's
+        // that look like they're coming from the system are attributed to the app, but when
+        // modes_ui is true, we opt to trust the provided change origin.
         mZenModeHelper.setAutomaticZenRuleState(id,
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                CUSTOM_PKG_UID);
 
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
         mZenModeHelper.updateAutomaticZenRule(id, zenRule,
-                Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
+                Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
                 Process.SYSTEM_UID);
 
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -3118,7 +3129,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule,
-                Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test",
+                Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test",
                 Process.SYSTEM_UID);
 
         // Event 3: turn on the system rule
@@ -3128,7 +3139,7 @@
 
         // Event 4: "User" deletes the rule
         mZenModeHelper.removeAutomaticZenRule(systemId,
-                Flags.modesApi() ? UPDATE_ORIGIN_USER: UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
+                Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "",
                 Process.SYSTEM_UID);
         // In total, this represents 4 events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
@@ -3151,9 +3162,13 @@
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
+        assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : 0);
 
         // When the automatic rule is disabled, this should turn off zen mode and also count as a
         // user action. We don't care what the consolidated policy is when DND turns off.
+        // When modes_ui is true, this event should look like a user action attributed to the
+        // specific app.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
@@ -3161,12 +3176,15 @@
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertTrue(mZenModeEventLogger.getIsUserAction(1));
-        assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1));
+        assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo(
+                Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID);
         if (Flags.modesApi()) {
             assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull();
         } else {
             checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
         }
+        assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_USER : 0);
 
         // When the system rule is enabled, this counts as an automatic action that comes from the
         // system and turns on DND
@@ -3176,6 +3194,8 @@
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(2));
         assertFalse(mZenModeEventLogger.getIsUserAction(2));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2));
+        assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI : 0);
 
         // When the system rule is deleted, we consider this a user action that turns DND off
         // (again)
@@ -3185,6 +3205,8 @@
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(3));
         assertTrue(mZenModeEventLogger.getIsUserAction(3));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(3));
+        assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_USER : 0);
     }
 
     @Test
@@ -3238,6 +3260,8 @@
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
+        assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_USER : 0);
 
         // Automatic rule turned off automatically by app:
         //   - event ID: DND_TURNED_OFF
@@ -3249,6 +3273,8 @@
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
+        assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : 0);
 
         // Automatic rule turned on automatically by app:
         //   - event ID: DND_TURNED_ON
@@ -3261,6 +3287,8 @@
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(2));
         assertFalse(mZenModeEventLogger.getIsUserAction(2));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2));
+        assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : 0);
 
         // Automatic rule turned off automatically by the user:
         //   - event ID: DND_TURNED_ON
@@ -3272,6 +3300,8 @@
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(3));
         assertTrue(mZenModeEventLogger.getIsUserAction(3));
         assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3));
+        assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo(
+                Flags.modesUi() ? UPDATE_ORIGIN_USER : 0);
     }
 
     @Test
@@ -3335,7 +3365,7 @@
 
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
                 null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                new ComponentName("android", "ScheduleConditionProvider"),
                 ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
@@ -3345,7 +3375,7 @@
         // Rule 2, same as rule 1
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
                 null,
-                new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+                new ComponentName("android", "ScheduleConditionProvider"),
                 ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
@@ -3395,7 +3425,7 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
-        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
+        assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
 
         // Event 2: rule 2 turns on. This should not change anything about the policy, so the only
@@ -3404,7 +3434,7 @@
                 mZenModeEventLogger.getEventId(1));
         assertEquals(2, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
-        assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
+        assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1));
         checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1));
 
         // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such,
@@ -3482,9 +3512,11 @@
         // Turn on rule 1; call looks like it's from the system. Because setting a condition is
         // typically an automatic (non-user-initiated) action, expect the calling UID to be
         // re-evaluated to the one associated with CUSTOM_PKG_NAME.
+        // When modes_ui is true: we expect the change origin to be the source of truth.
         mZenModeHelper.setAutomaticZenRuleState(id,
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+                Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                Process.SYSTEM_UID);
 
         // Second: turn on rule 2. This is a system-owned rule and the UID should not be modified
         // (nor even looked up; the mock PackageManager won't handle "android" as input).
@@ -3493,7 +3525,7 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Disable rule 1. Because this looks like a user action, the UID should not be modified
-        // from the system-provided one.
+        // from the system-provided one unless modes_ui is true.
         zenRule.setEnabled(false);
         mZenModeHelper.updateAutomaticZenRule(id, zenRule,
                 UPDATE_ORIGIN_USER, "", Process.SYSTEM_UID);
@@ -3504,6 +3536,7 @@
 
         // Change rule 2's condition, but from some other UID. Since it doesn't look like it's from
         // the system, we keep the UID info.
+        // Note that this probably shouldn't be able to occur in real scenarios.
         mZenModeHelper.setAutomaticZenRuleState(id2,
                 new Condition(zenRule2.getConditionId(), "", STATE_FALSE),
                 UPDATE_ORIGIN_APP, 12345);
@@ -3528,11 +3561,13 @@
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1));
 
         // Third event: disable rule 1. This looks like a user action so UID should be left alone.
+        // When modes_ui is true, we assign log this user action with the app that owns the rule.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId(),
                 mZenModeEventLogger.getEventId(2));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2));
         assertTrue(mZenModeEventLogger.getIsUserAction(2));
-        assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2));
+        assertThat(mZenModeEventLogger.getPackageUid(2)).isEqualTo(
+                Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID);
 
         // Fourth event: turns on manual mode. Doesn't change effective policy so this is just a
         // change in active rules. Confirm that the package UID is left unchanged.
@@ -6202,7 +6237,7 @@
     public void setManualZenRuleDeviceEffects_noPreexistingMode() {
         ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
                 .setShouldDimWallpaper(true)
-                        .build();
+                .build();
         mZenModeHelper.setManualZenRuleDeviceEffects(effects, UPDATE_ORIGIN_USER, "settings", 1000);
 
         assertThat(mZenModeHelper.getConfig().manualRule).isNotNull();
@@ -6339,21 +6374,21 @@
 
     private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA =
             Correspondence.transforming(zr -> {
-                Parcel p = Parcel.obtain();
-                try {
-                    zr.writeToParcel(p, 0);
-                    p.setDataPosition(0);
-                    ZenRule copy = new ZenRule(p);
-                    copy.creationTime = 0;
-                    copy.userModifiedFields = 0;
-                    copy.zenPolicyUserModifiedFields = 0;
-                    copy.zenDeviceEffectsUserModifiedFields = 0;
-                    return copy;
-                } finally {
-                    p.recycle();
-                }
-            },
-              "Ignoring timestamp and userModifiedFields");
+                        Parcel p = Parcel.obtain();
+                        try {
+                            zr.writeToParcel(p, 0);
+                            p.setDataPosition(0);
+                            ZenRule copy = new ZenRule(p);
+                            copy.creationTime = 0;
+                            copy.userModifiedFields = 0;
+                            copy.zenPolicyUserModifiedFields = 0;
+                            copy.zenDeviceEffectsUserModifiedFields = 0;
+                            return copy;
+                        } finally {
+                            p.recycle();
+                        }
+                    },
+                    "Ignoring timestamp and userModifiedFields");
 
     private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
             @Nullable Boolean conditionActive) {
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 777f618..6e6b70d 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -52,6 +52,7 @@
     <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS"/>
     <uses-permission android:name="android.permission.MANAGE_DEFAULT_APPLICATIONS"/>
     <uses-permission android:name="android.permission.DUMP"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) -->
     <application android:debuggable="true"
diff --git a/services/tests/wmtests/res/xml/bookmarks.xml b/services/tests/wmtests/res/xml/bookmarks.xml
new file mode 100644
index 0000000..88419e9
--- /dev/null
+++ b/services/tests/wmtests/res/xml/bookmarks.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<bookmarks>
+    <bookmark
+        role="android.app.role.BROWSER"
+        shortcut="b" />
+    <bookmark
+        category="android.intent.category.APP_CONTACTS"
+        shortcut="c" />
+    <bookmark
+        category="android.intent.category.APP_EMAIL"
+        shortcut="e" />
+    <bookmark
+        category="android.intent.category.APP_CALENDAR"
+        shortcut="k" />
+    <bookmark
+        category="android.intent.category.APP_MAPS"
+        shortcut="m" />
+    <bookmark
+        category="android.intent.category.APP_MUSIC"
+        shortcut="p" />
+    <bookmark
+        role="android.app.role.SMS"
+        shortcut="s" />
+    <bookmark
+        category="android.intent.category.APP_CALCULATOR"
+        shortcut="u" />
+</bookmarks>
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
new file mode 100644
index 0000000..8c375d4
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2024 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.policy;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+/**
+ * Test class for {@link ModifierShortcutManager}.
+ *
+ * Build/Install/Run:
+ *  atest ModifierShortcutManagerTests
+ */
+
+@SmallTest
+public class ModifierShortcutManagerTests {
+    private ModifierShortcutManager mModifierShortcutManager;
+    private Handler mHandler;
+    private Context mContext;
+    private Resources mResources;
+
+    @Before
+    public void setUp() {
+        mHandler = new Handler(Looper.getMainLooper());
+        mContext = spy(getInstrumentation().getTargetContext());
+        mResources = spy(mContext.getResources());
+
+        XmlResourceParser testBookmarks = mResources.getXml(
+                com.android.frameworks.wmtests.R.xml.bookmarks);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks);
+
+        mModifierShortcutManager = new ModifierShortcutManager(mContext, mHandler);
+    }
+
+    @Test
+    public void test_getApplicationLaunchKeyboardShortcuts() {
+        KeyboardShortcutGroup group =
+                mModifierShortcutManager.getApplicationLaunchKeyboardShortcuts(-1);
+        assertEquals(8, group.getItems().size());
+    }
+
+    @Test
+    public void test_shortcutInfoFromIntent_appIntent() {
+        Intent mockIntent = mock(Intent.class);
+        ActivityInfo mockActivityInfo = mock(ActivityInfo.class);
+        when(mockActivityInfo.loadLabel(anyObject())).thenReturn("label");
+        mockActivityInfo.packageName = "android";
+        when(mockActivityInfo.getIconResource()).thenReturn(R.drawable.sym_def_app_icon);
+        when(mockIntent.resolveActivityInfo(anyObject(), anyInt())).thenReturn(mockActivityInfo);
+
+        KeyboardShortcutInfo info = mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, true);
+
+        assertEquals("label", info.getLabel().toString());
+        assertEquals('a', info.getBaseCharacter());
+        assertEquals(R.drawable.sym_def_app_icon, info.getIcon().getResId());
+        assertEquals(KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON, info.getModifiers());
+
+    }
+
+    @Test
+    public void test_shortcutInfoFromIntent_resolverIntent() {
+        Intent mockIntent = mock(Intent.class);
+        Intent mockSelector = mock(Intent.class);
+        ActivityInfo mockActivityInfo = mock(ActivityInfo.class);
+        mockActivityInfo.name = com.android.internal.app.ResolverActivity.class.getName();
+        when(mockIntent.resolveActivityInfo(anyObject(), anyInt())).thenReturn(mockActivityInfo);
+        when(mockIntent.getSelector()).thenReturn(mockSelector);
+        when(mockSelector.getCategories()).thenReturn(
+                Collections.singleton(Intent.CATEGORY_APP_BROWSER));
+
+        KeyboardShortcutInfo info = mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, false);
+
+        assertEquals(mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                info.getLabel().toString());
+        assertEquals('a', info.getBaseCharacter());
+        assertEquals(R.drawable.sym_def_app_icon, info.getIcon().getResId());
+        assertEquals(KeyEvent.META_META_ON, info.getModifiers());
+
+        // validate that an unknown category that we can't present a label to the user for
+        // returns null shortcut info.
+        when(mockSelector.getCategories()).thenReturn(
+                Collections.singleton("not_a_category"));
+        assertEquals(null,  mModifierShortcutManager.shortcutInfoFromIntent(
+                'a', mockIntent, false));
+    }
+
+    @Test
+    public void test_getIntentCategoryLabel() {
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_BROWSER));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CONTACTS));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_email),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_EMAIL));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CALENDAR));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_maps),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MAPS));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_music),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MUSIC));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_MESSAGING));
+        assertEquals(
+                mContext.getString(R.string.keyboard_shortcut_group_applications_calculator),
+                ModifierShortcutManager.getIntentCategoryLabel(
+                    mContext, Intent.CATEGORY_APP_CALCULATOR));
+        assertEquals(null, ModifierShortcutManager.getIntentCategoryLabel(mContext, "foo"));
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 0ed02dd..59b08a5 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -65,7 +65,8 @@
     private static final SparseArray<String> INTENT_SHORTCUTS =  new SparseArray<>();
     private static final SparseArray<String> ROLE_SHORTCUTS =  new SparseArray<>();
     static {
-        // These shortcuts should align with those defined in bookmarks.xml
+        // These shortcuts should align with those defined in
+        // services/tests/wmtests/res/xml/bookmarks.xml
         INTENT_SHORTCUTS.append(KEYCODE_U, Intent.CATEGORY_APP_CALCULATOR);
         INTENT_SHORTCUTS.append(KEYCODE_C, Intent.CATEGORY_APP_CONTACTS);
         INTENT_SHORTCUTS.append(KEYCODE_E, Intent.CATEGORY_APP_EMAIL);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index dff4984..f5c8fb8 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -49,6 +49,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArrayMap;
 import android.view.InputDevice;
@@ -102,6 +103,9 @@
         doReturn(mResources).when(mContext).getResources();
         doReturn(mSettingsProviderRule.mockContentResolver(mContext))
                 .when(mContext).getContentResolver();
+        XmlResourceParser testBookmarks = mResources.getXml(
+                com.android.frameworks.wmtests.R.xml.bookmarks);
+        doReturn(testBookmarks).when(mResources).getXml(com.android.internal.R.xml.bookmarks);
     }
 
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
index a4df034..c9c7e92 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java
@@ -18,9 +18,11 @@
 
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
 import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_DISALLOW;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BOUND_BY_FOREGROUND;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_TOKEN;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
 
@@ -30,12 +32,18 @@
 import android.content.Context;
 import android.os.Binder;
 import android.os.IBinder;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.wm.BackgroundActivityStartController.BalVerdict;
+import com.android.window.flags.Flags;
 
+import org.junit.ClassRule;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -44,6 +52,7 @@
 import java.util.HashSet;
 import java.util.Set;
 
+
 /**
  * Tests for the {@link BackgroundLaunchProcessController} class.
  *
@@ -55,6 +64,10 @@
 @RunWith(JUnit4.class)
 public class BackgroundLaunchProcessControllerTests {
 
+
+    @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+    @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
     Set<IBinder> mActivityStartAllowed = new HashSet<>();
     Set<Integer> mHasActiveVisibleWindow = new HashSet<>();
 
@@ -113,7 +126,8 @@
     }
 
     @Test
-    public void testAllowedByTokenNoCallback() {
+    @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testAllowedByTokenNoCallbackOld() {
         mController = new BackgroundLaunchProcessController(mHasActiveVisibleWindow::contains,
                 null);
         Binder token = new Binder();
@@ -130,7 +144,26 @@
     }
 
     @Test
-    public void testAllowedByToken() {
+    @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testAllowedByTokenNoCallback() {
+        mController = new BackgroundLaunchProcessController(mHasActiveVisibleWindow::contains,
+                null);
+        Binder token = new Binder();
+        mActivityStartAllowed.add(token);
+        mController.addOrUpdateAllowBackgroundStartPrivileges(token,
+                BackgroundStartPrivileges.ALLOW_BAL);
+        BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed(
+                mPid, mUid, mPackageName,
+                mAppSwitchState, mIsCheckingForFgsStart,
+                mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges,
+                mLastStopAppSwitchesTime, mLastActivityLaunchTime,
+                mLastActivityFinishTime);
+        assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_TOKEN);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testAllowedByTokenOld() {
         Binder token = new Binder();
         mActivityStartAllowed.add(token);
         mController.addOrUpdateAllowBackgroundStartPrivileges(token,
@@ -145,7 +178,24 @@
     }
 
     @Test
-    public void testBoundByForeground() {
+    @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testAllowedByToken() {
+        Binder token = new Binder();
+        mActivityStartAllowed.add(token);
+        mController.addOrUpdateAllowBackgroundStartPrivileges(token,
+                BackgroundStartPrivileges.ALLOW_BAL);
+        BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed(
+                mPid, mUid, mPackageName,
+                mAppSwitchState, mIsCheckingForFgsStart,
+                mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges,
+                mLastStopAppSwitchesTime, mLastActivityLaunchTime,
+                mLastActivityFinishTime);
+        assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_TOKEN);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testBoundByForegroundOld() {
         mAppSwitchState = APP_SWITCH_ALLOW;
         mController.addBoundClientUid(999, "visible.package", Context.BIND_ALLOW_ACTIVITY_STARTS);
         mHasActiveVisibleWindow.add(999);
@@ -159,6 +209,21 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_BAL_IMPROVED_METRICS)
+    public void testBoundByForeground() {
+        mAppSwitchState = APP_SWITCH_ALLOW;
+        mController.addBoundClientUid(999, "visible.package", Context.BIND_ALLOW_ACTIVITY_STARTS);
+        mHasActiveVisibleWindow.add(999);
+        BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed(
+                mPid, mUid, mPackageName,
+                mAppSwitchState, mIsCheckingForFgsStart,
+                mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges,
+                mLastStopAppSwitchesTime, mLastActivityLaunchTime,
+                mLastActivityFinishTime);
+        assertThat(balVerdict.getCode()).isEqualTo(BAL_ALLOW_BOUND_BY_FOREGROUND);
+    }
+
+    @Test
     public void testForegroundTask() {
         mAppSwitchState = APP_SWITCH_ALLOW;
         mHasActivityInVisibleTask = true;
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index d6d8339..d29505f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -274,17 +274,28 @@
 
     @Test
     public void testAttachApplication() {
-        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setProcessName("testAttach")
+                .setCreateTask(true).build();
+        final ActivityRecord topActivity = new ActivityBuilder(mAtm).setProcessName("testAttach")
+                .setUseProcess(activity.app).setTask(activity.getTask()).build();
         activity.detachFromProcess();
-        mAtm.startProcessAsync(activity, false /* knownToBeDead */,
+        topActivity.detachFromProcess();
+        mAtm.startProcessAsync(topActivity, false /* knownToBeDead */,
                 true /* isTop */, "test" /* hostingType */);
+        // Even if the activity is added after topActivity, the start order should still follow
+        // z-order, i.e. the topActivity will be started first.
+        mAtm.startProcessAsync(activity, false /* knownToBeDead */,
+                false /* isTop */, "test" /* hostingType */);
+        assertEquals(2, mAtm.mStartingProcessActivities.size());
+        assertEquals("Top record must be at the tail to start first",
+                topActivity, mAtm.mStartingProcessActivities.get(1));
         final WindowProcessController proc = mSystemServicesTestRule.addProcess(
                 activity.packageName, activity.processName,
                 6789 /* pid */, activity.info.applicationInfo.uid);
         try {
             mRootWindowContainer.attachApplication(proc);
-            verify(mSupervisor).realStartActivityLocked(eq(activity), eq(proc), anyBoolean(),
-                    anyBoolean());
+            verify(mSupervisor).realStartActivityLocked(eq(topActivity), eq(proc),
+                    anyBoolean(), anyBoolean());
         } catch (RemoteException e) {
             e.rethrowAsRuntimeException();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 5fe8524..ae88b1b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -104,6 +104,7 @@
 import android.os.Binder;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
@@ -495,7 +496,7 @@
         // Activity is sandboxed; it is in size compat mode since it is not resizable and has a
         // max aspect ratio.
         assertActivityMaxBoundsSandboxed();
-        assertScaled();
+        assertDownScaled();
     }
 
     @Test
@@ -515,7 +516,7 @@
         // The bounds should be [100, 0 - 1100, 2500].
         assertEquals(origBounds.width(), currentBounds.width());
         assertEquals(origBounds.height(), currentBounds.height());
-        assertScaled();
+        assertDownScaled();
 
         // The scale is 2000/2500=0.8. The horizontal centered offset is (1000-(1000*0.8))/2=100.
         final float scale = (float) display.mBaseDisplayHeight / currentBounds.height();
@@ -555,7 +556,7 @@
         assertEquals(origBounds.width(), currentBounds.width());
         assertEquals(origBounds.height(), currentBounds.height());
         assertEquals(offsetX, mActivity.getBounds().left);
-        assertScaled();
+        assertDownScaled();
         // Activity is sandboxed due to size compat mode.
         assertActivityMaxBoundsSandboxed();
 
@@ -693,7 +694,7 @@
         // The configuration bounds [820, 0 - 1820, 2500] should keep the same.
         assertEquals(originalBounds.width(), currentBounds.width());
         assertEquals(originalBounds.height(), currentBounds.height());
-        assertScaled();
+        assertDownScaled();
         // Activity max bounds are sandboxed due to size compat mode on the new display.
         assertActivityMaxBoundsSandboxed();
 
@@ -752,7 +753,7 @@
         assertEquals(origAppBounds.width(), appBounds.width());
         assertEquals(origAppBounds.height(), appBounds.height());
         // The activity is 1000x1400 and the display is 2500x1000.
-        assertScaled();
+        assertDownScaled();
         final float scale = mActivity.getCompatScale();
         // The position in configuration should be in app coordinates.
         final Rect screenBounds = mActivity.getBounds();
@@ -849,7 +850,7 @@
         // Size compatibility mode is able to handle orientation change so the process shouldn't be
         // restarted and the override configuration won't be cleared.
         verify(mActivity, never()).restartProcessIfVisible();
-        assertScaled();
+        assertDownScaled();
         // Activity max bounds are sandboxed due to size compat mode, even if is not visible.
         assertActivityMaxBoundsSandboxed();
 
@@ -1624,6 +1625,85 @@
                 activity.getBounds().width(), 0.5);
     }
 
+
+    /**
+     * Test that a freeform unresizeable activity can be down-scaled to fill its smaller parent
+     * bounds.
+     */
+    @Test
+    public void testCompatScaling_freeformUnresizeableApp_largerThanParent_downScaled() {
+        final int dw = 600;
+        final int dh = 800;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM)
+                .build();
+        setUpApp(display);
+        prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertFalse(mActivity.inSizeCompatMode());
+
+        // Resize app to make original app bounds larger than parent bounds.
+        mTask.getWindowConfiguration().setAppBounds(
+                new Rect(0, 0, dw - 300, dh - 400));
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // App should enter size compat mode and be down-scaled to fill new parent bounds.
+        assertDownScaled();
+    }
+
+    /**
+     * Test that when desktop mode is enabled, a freeform unresizeable activity can be up-scaled to
+     * fill its larger parent bounds.
+     */
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void testCompatScaling_freeformUnresizeableApp_smallerThanParent_upScaled() {
+        doReturn(true).when(() ->
+                DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+        final int dw = 600;
+        final int dh = 800;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM)
+                .build();
+        setUpApp(display);
+        prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertFalse(mActivity.inSizeCompatMode());
+
+        // Resize app to make original app bounds smaller than parent bounds.
+        mTask.getWindowConfiguration().setAppBounds(
+                new Rect(0, 0, dw + 300, dh + 400));
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // App should enter size compat mode and be up-scaled to fill parent bounds.
+        assertUpScaled();
+    }
+
+    /**
+     * Test that when desktop mode is disabled, a freeform unresizeable activity cannot be up-scaled
+     * despite its larger parent bounds.
+     */
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void testSizeCompatScaling_freeformUnresizeableApp_smallerThanParent_notScaled() {
+        final int dw = 600;
+        final int dh = 800;
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM)
+                .build();
+        setUpApp(display);
+        prepareUnresizable(mActivity, /* maxAspect */ 0f, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        assertFalse(mActivity.inSizeCompatMode());
+        final Rect originalAppBounds = mActivity.getBounds();
+
+        // Resize app to make original app bounds smaller than parent bounds.
+        mTask.getWindowConfiguration().setAppBounds(
+                new Rect(0, 0, dw + 300, dh + 400));
+        mActivity.onConfigurationChanged(mTask.getConfiguration());
+        // App should enter size compat mode but remain its original size.
+        assertTrue(mActivity.inSizeCompatMode());
+        assertEquals(originalAppBounds, mActivity.getBounds());
+    }
+
     @Test
     public void testGetLetterboxInnerBounds_noScalingApplied() {
         // Set up a display in portrait and ignoring orientation request.
@@ -1659,7 +1739,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertThat(mActivity.inSizeCompatMode()).isTrue();
         assertActivityMaxBoundsSandboxed();
 
@@ -2000,7 +2080,7 @@
 
         // After we rotate, the activity should go in the size-compat mode and report the same
         // configuration values.
-        assertScaled();
+        assertDownScaled();
         assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp);
         assertEquals(originalScreenWidthDp, mActivity.getConfiguration().screenWidthDp);
         assertEquals(originalScreenHeighthDp, mActivity.getConfiguration().screenHeightDp);
@@ -2775,7 +2855,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertEquals(activityBounds.width(), newActivityBounds.width());
         assertEquals(activityBounds.height(), newActivityBounds.height());
         assertActivityMaxBoundsSandboxed();
@@ -2807,7 +2887,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertThat(mActivity.inSizeCompatMode()).isTrue();
         assertActivityMaxBoundsSandboxed();
 
@@ -2955,7 +3035,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertThat(mActivity.inSizeCompatMode()).isTrue();
         // Activity max bounds are sandboxed due to size compat mode.
         assertActivityMaxBoundsSandboxed();
@@ -2967,7 +3047,7 @@
         verify(mActivity, never()).clearSizeCompatMode();
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertEquals(activityBounds, mActivity.getBounds());
         // Activity max bounds are sandboxed due to size compat.
         assertActivityMaxBoundsSandboxed();
@@ -2995,7 +3075,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertActivityMaxBoundsSandboxed();
 
         // Rotate display to landscape.
@@ -3032,7 +3112,7 @@
         // App should be in size compat.
         assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy()
                 .isLetterboxedForFixedOrientationAndAspectRatio());
-        assertScaled();
+        assertDownScaled();
         assertActivityMaxBoundsSandboxed();
 
         // Rotate display to portrait.
@@ -3224,7 +3304,7 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Non-resizable activity in size compat mode
-        assertScaled();
+        assertDownScaled();
         final Rect newBounds = new Rect(mActivity.getWindowConfiguration().getBounds());
         assertEquals(originalBounds.width(), newBounds.width());
         assertEquals(originalBounds.height(), newBounds.height());
@@ -3288,7 +3368,7 @@
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
 
         // Non-resizable activity in size compat mode
-        assertScaled();
+        assertDownScaled();
         final Rect newBounds = new Rect(mActivity.getWindowConfiguration().getBounds());
         assertEquals(originalBounds.width(), newBounds.width());
         assertEquals(originalBounds.height(), newBounds.height());
@@ -3329,7 +3409,7 @@
         organizer.mPrimary.setBounds(0, 0, 1000, 800);
 
         // Non-resizable activity should be in size compat mode.
-        assertScaled();
+        assertDownScaled();
         assertEquals(mActivity.getBounds(), new Rect(60, 0, 940, 800));
 
         recomputeNaturalConfigurationOfUnresizableActivity();
@@ -3906,7 +3986,7 @@
         // Force activity to scaled down for size compat mode.
         resizeDisplay(mTask.mDisplayContent, 700, 1400);
         assertTrue(mActivity.inSizeCompatMode());
-        assertScaled();
+        assertDownScaled();
         assertEquals(sizeCompatScaled, mActivity.getBounds());
     }
 
@@ -4406,7 +4486,7 @@
         resizeDisplay(mTask.mDisplayContent, 1400, 700);
 
         assertTrue(mActivity.inSizeCompatMode());
-        assertScaled();
+        assertDownScaled();
         assertEquals(sizeCompatScaled, mActivity.getBounds());
     }
 
@@ -4672,7 +4752,7 @@
         // Target min aspect ratio must be larger than parent aspect ratio to be applied.
         final float targetMinAspectRatio = 3.0f;
 
-        // Create fixed portait activity with min aspect ratio greater than parent aspect ratio.
+        // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio.
         final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
                 .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
                 .setMinAspectRatio(targetMinAspectRatio).build();
@@ -4686,7 +4766,7 @@
         final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
                 .windowConfiguration.getAppBounds());
 
-        // Create unresizeable fixed portait activity with min aspect ratio greater than parent
+        // Create unresizeable fixed portrait activity with min aspect ratio greater than parent
         // aspect ratio.
         final ActivityRecord sizeCompatActivity = new ActivityBuilder(mAtm)
                 .setTask(task).setResizeMode(RESIZE_MODE_UNRESIZEABLE)
@@ -4719,7 +4799,7 @@
         // Activity should enter size compat with old density after display density change.
         display.setForcedDensity(newDensity, UserHandle.USER_CURRENT);
 
-        assertScaled();
+        assertDownScaled();
         assertEquals(origDensity, mActivity.getConfiguration().densityDpi);
 
         // Activity should exit size compat with new density.
@@ -4958,14 +5038,25 @@
         }
     }
 
-    private void assertScaled() {
-        assertScaled(mActivity);
+    private void assertUpScaled() {
+        assertScaled(mActivity, /* upScalingExpected */ true);
     }
 
-    /** Asserts that the size of activity is larger than its parent so it is scaling. */
-    private void assertScaled(ActivityRecord activity) {
+    private void assertDownScaled() {
+        assertScaled(mActivity, /* upScalingExpected */ false);
+    }
+
+    /**
+     * Asserts that the size of an activity differs from its parent and so it is scaling (either up
+     * or down).
+     */
+    private void assertScaled(ActivityRecord activity, boolean upScalingExpected) {
         assertTrue(activity.inSizeCompatMode());
-        assertNotEquals(1f, activity.getCompatScale(), 0.0001f /* delta */);
+        if (upScalingExpected) {
+            assertTrue(activity.getCompatScale() > 1f);
+        } else {
+            assertTrue(activity.getCompatScale() < 1f);
+        }
     }
 
     private void assertFitted() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 00a8842..38ad9a7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -27,6 +27,7 @@
 import android.os.PowerManager.WakeReason;
 import android.util.proto.ProtoOutputStream;
 import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
 import android.view.WindowManager;
 import android.view.animation.Animation;
 
@@ -35,6 +36,7 @@
 import com.android.server.policy.WindowManagerPolicy;
 
 import java.io.PrintWriter;
+import java.util.Collections;
 
 class TestWindowManagerPolicy implements WindowManagerPolicy {
 
@@ -362,4 +364,9 @@
     public boolean isGlobalKey(int keyCode) {
         return false;
     }
+
+    @Override
+    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
+        return new KeyboardShortcutGroup("", Collections.emptyList());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 45dbea2..401964c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -805,18 +805,11 @@
 
         final TestWindowContainer root2 = builder.setLayer(0).build();
 
+        assertEquals("Roots have the same z-order", 0, root.compareTo(root2));
         assertEquals(0, root.compareTo(root));
         assertEquals(-1, child1.compareTo(child2));
         assertEquals(1, child2.compareTo(child1));
 
-        boolean inTheSameTree = true;
-        try {
-            root.compareTo(root2);
-        } catch (IllegalArgumentException e) {
-            inTheSameTree = false;
-        }
-        assertFalse(inTheSameTree);
-
         assertEquals(-1, child1.compareTo(child11));
         assertEquals(1, child21.compareTo(root));
         assertEquals(1, child21.compareTo(child12));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index fcf7a3f..4958b90 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -1294,8 +1294,6 @@
 
     @Test
     public void testRelayout_appWindowSendActivityWindowInfo() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
-
         // Skip unnecessary operations of relayout.
         spyOn(mWm.mWindowPlacerLocked);
         doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
similarity index 97%
rename from services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java
rename to services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
index c183403..48a8d55 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingLegacyTest.java
@@ -63,7 +63,7 @@
  */
 @SmallTest
 @Presubmit
-public class WindowTracingTest {
+public class WindowTracingLegacyTest {
 
     private static final byte[] MAGIC_HEADER = new byte[]{
             0x9, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
@@ -88,7 +88,7 @@
         mFile = testContext.getFileStreamPath("tracing_test.dat");
         mFile.delete();
 
-        mWindowTracing = new WindowTracing(mFile, mWmMock, mChoreographer,
+        mWindowTracing = new WindowTracingLegacy(mFile, mWmMock, mChoreographer,
                 new WindowManagerGlobalLock(), 1024);
     }
 
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index b518c60d..aebae4e 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -50,7 +50,6 @@
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -2636,9 +2635,9 @@
                         if (resultCode == SATELLITE_RESULT_SUCCESS) {
                             if (resultData.containsKey(KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN)) {
                                 List<ProvisionSubscriberId> list =
-                                        Collections.singletonList(resultData.getParcelable(
+                                        resultData.getParcelableArrayList(
                                                 KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN,
-                                                ProvisionSubscriberId.class));
+                                                ProvisionSubscriberId.class);
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                         callback.onResult(list)));
                             } else {
@@ -2692,13 +2691,13 @@
                     @Override
                     protected void onReceiveResult(int resultCode, Bundle resultData) {
                         if (resultCode == SATELLITE_RESULT_SUCCESS) {
-                            if (resultData.containsKey(KEY_SATELLITE_PROVISIONED)) {
+                            if (resultData.containsKey(KEY_IS_SATELLITE_PROVISIONED)) {
                                 boolean isIsProvisioned =
-                                        resultData.getBoolean(KEY_SATELLITE_PROVISIONED);
+                                        resultData.getBoolean(KEY_IS_SATELLITE_PROVISIONED);
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                         callback.onResult(isIsProvisioned)));
                             } else {
-                                loge("KEY_REQUEST_PROVISION_TOKENS does not exist.");
+                                loge("KEY_IS_SATELLITE_PROVISIONED does not exist.");
                                 executor.execute(() -> Binder.withCleanCallingIdentity(() ->
                                         callback.onError(new SatelliteException(
                                                 SATELLITE_RESULT_REQUEST_FAILED))));
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 7be52ea..3dbda7a 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3411,7 +3411,7 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
-    void requestProvisionSubscriberIds(in ResultReceiver receiver);
+    void requestProvisionSubscriberIds(in ResultReceiver result);
 
     /**
      * Request to get provisioned status for given a satellite subscriber id.
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 9a5e88b..238f2af 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -165,4 +165,37 @@
             Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom)
         }
     }
+
+    /** Exit desktop mode by dragging the app handle to the top drag zone. */
+    fun exitDesktopWithDragToTopDragZone(
+        wmHelper: WindowManagerStateHelper,
+        device: UiDevice,
+    ) {
+        dragAppWindowToTopDragZone(wmHelper, device)
+        waitForTransitionToFullscreen(wmHelper)
+    }
+
+    private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+        val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
+        val displayRect =
+            wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
+                ?: throw IllegalStateException("Default display is null")
+
+        val startX = windowRect.centerX()
+        val endX = displayRect.centerX()
+        val startY = windowRect.top
+        val endY = 0 // top of the screen
+
+        // drag the app window to top drag zone
+        device.drag(startX, startY, endX, endY, 100)
+    }
+
+    /** Wait for transition to full screen to finish. */
+    private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) {
+        wmHelper
+            .StateSyncBuilder()
+            .withFullScreenApp(innerHelper)
+            .withAppTransitionIdle()
+            .waitForAndVerify()
+    }
 }
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java
new file mode 100644
index 0000000..2539653
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.zip.GZIPOutputStream;
+
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class LegacyProtoLogViewerConfigReaderTest {
+    private static final String TEST_VIEWER_CONFIG = "{\n"
+            + "  \"version\": \"1.0.0\",\n"
+            + "  \"messages\": {\n"
+            + "    \"70933285\": {\n"
+            + "      \"message\": \"Test completed successfully: %b\",\n"
+            + "      \"level\": \"ERROR\",\n"
+            + "      \"group\": \"GENERIC_WM\"\n"
+            + "    },\n"
+            + "    \"1792430067\": {\n"
+            + "      \"message\": \"Attempted to add window to a display that does not exist: %d."
+            + "  Aborting.\",\n"
+            + "      \"level\": \"WARN\",\n"
+            + "      \"group\": \"GENERIC_WM\"\n"
+            + "    },\n"
+            + "    \"1352021864\": {\n"
+            + "      \"message\": \"Test 2\",\n"
+            + "      \"level\": \"WARN\",\n"
+            + "      \"group\": \"GENERIC_WM\"\n"
+            + "    },\n"
+            + "    \"409412266\": {\n"
+            + "      \"message\": \"Window %s is already added\",\n"
+            + "      \"level\": \"WARN\",\n"
+            + "      \"group\": \"GENERIC_WM\"\n"
+            + "    }\n"
+            + "  },\n"
+            + "  \"groups\": {\n"
+            + "    \"GENERIC_WM\": {\n"
+            + "      \"tag\": \"WindowManager\"\n"
+            + "    }\n"
+            + "  }\n"
+            + "}\n";
+
+
+    private LegacyProtoLogViewerConfigReader
+            mConfig = new LegacyProtoLogViewerConfigReader();
+    private File mTestViewerConfig;
+
+    @Before
+    public void setUp() throws IOException {
+        mTestViewerConfig = File.createTempFile("testConfig", ".json.gz");
+        OutputStreamWriter writer = new OutputStreamWriter(
+                new GZIPOutputStream(new FileOutputStream(mTestViewerConfig)));
+        writer.write(TEST_VIEWER_CONFIG);
+        writer.close();
+    }
+
+    @After
+    public void tearDown() {
+        //noinspection ResultOfMethodCallIgnored
+        mTestViewerConfig.delete();
+    }
+
+    @Test
+    public void getViewerString_notLoaded() {
+        assertNull(mConfig.getViewerString(1));
+    }
+
+    @Test
+    public void loadViewerConfig() {
+        mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath());
+        assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285));
+        assertEquals("Test 2", mConfig.getViewerString(1352021864));
+        assertEquals("Window %s is already added", mConfig.getViewerString(409412266));
+        assertNull(mConfig.getViewerString(1));
+    }
+
+    @Test
+    public void loadViewerConfig_invalidFile() {
+        mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist");
+        // No exception is thrown.
+        assertNull(mConfig.getViewerString(1));
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
index dbd85d3..be0e8bc 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java
@@ -20,75 +20,77 @@
 import static org.junit.Assert.assertNull;
 
 import android.platform.test.annotations.Presubmit;
+import android.util.proto.ProtoInputStream;
 
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.zip.GZIPOutputStream;
+import perfetto.protos.Protolog;
+import perfetto.protos.ProtologCommon;
 
-@SmallTest
 @Presubmit
 @RunWith(JUnit4.class)
 public class ProtoLogViewerConfigReaderTest {
-    private static final String TEST_VIEWER_CONFIG = "{\n"
-            + "  \"version\": \"1.0.0\",\n"
-            + "  \"messages\": {\n"
-            + "    \"70933285\": {\n"
-            + "      \"message\": \"Test completed successfully: %b\",\n"
-            + "      \"level\": \"ERROR\",\n"
-            + "      \"group\": \"GENERIC_WM\"\n"
-            + "    },\n"
-            + "    \"1792430067\": {\n"
-            + "      \"message\": \"Attempted to add window to a display that does not exist: %d."
-            + "  Aborting.\",\n"
-            + "      \"level\": \"WARN\",\n"
-            + "      \"group\": \"GENERIC_WM\"\n"
-            + "    },\n"
-            + "    \"1352021864\": {\n"
-            + "      \"message\": \"Test 2\",\n"
-            + "      \"level\": \"WARN\",\n"
-            + "      \"group\": \"GENERIC_WM\"\n"
-            + "    },\n"
-            + "    \"409412266\": {\n"
-            + "      \"message\": \"Window %s is already added\",\n"
-            + "      \"level\": \"WARN\",\n"
-            + "      \"group\": \"GENERIC_WM\"\n"
-            + "    }\n"
-            + "  },\n"
-            + "  \"groups\": {\n"
-            + "    \"GENERIC_WM\": {\n"
-            + "      \"tag\": \"WindowManager\"\n"
-            + "    }\n"
-            + "  }\n"
-            + "}\n";
+    private static final String TEST_GROUP_NAME = "MY_TEST_GROUP";
+    private static final String TEST_GROUP_TAG = "TEST";
 
+    private static final String OTHER_TEST_GROUP_NAME = "MY_OTHER_TEST_GROUP";
+    private static final String OTHER_TEST_GROUP_TAG = "OTHER_TEST";
 
-    private LegacyProtoLogViewerConfigReader
-            mConfig = new LegacyProtoLogViewerConfigReader();
-    private File mTestViewerConfig;
+    private static final byte[] TEST_VIEWER_CONFIG =
+            perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder()
+                .addGroups(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder()
+                                .setId(1)
+                                .setName(TEST_GROUP_NAME)
+                                .setTag(TEST_GROUP_TAG)
+                ).addGroups(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder()
+                                .setId(1)
+                                .setName(OTHER_TEST_GROUP_NAME)
+                                .setTag(OTHER_TEST_GROUP_TAG)
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(1)
+                                .setMessage("My Test Log Message 1 %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG)
+                                .setGroupId(1)
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(2)
+                                .setMessage("My Test Log Message 2 %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE)
+                                .setGroupId(1)
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(3)
+                                .setMessage("My Test Log Message 3 %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN)
+                                .setGroupId(1)
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(4)
+                                .setMessage("My Test Log Message 4 %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR)
+                                .setGroupId(2)
+                ).addMessages(
+                        perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder()
+                                .setMessageId(5)
+                                .setMessage("My Test Log Message 5 %b")
+                                .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF)
+                                .setGroupId(2)
+                ).build().toByteArray();
+
+    private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider =
+            () -> new ProtoInputStream(TEST_VIEWER_CONFIG);
+
+    private ProtoLogViewerConfigReader mConfig;
 
     @Before
-    public void setUp() throws IOException {
-        mTestViewerConfig = File.createTempFile("testConfig", ".json.gz");
-        OutputStreamWriter writer = new OutputStreamWriter(
-                new GZIPOutputStream(new FileOutputStream(mTestViewerConfig)));
-        writer.write(TEST_VIEWER_CONFIG);
-        writer.close();
-    }
-
-    @After
-    public void tearDown() {
-        //noinspection ResultOfMethodCallIgnored
-        mTestViewerConfig.delete();
+    public void before() {
+        mConfig = new ProtoLogViewerConfigReader(mViewerConfigInputStreamProvider);
     }
 
     @Test
@@ -98,17 +100,26 @@
 
     @Test
     public void loadViewerConfig() {
-        mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath());
-        assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285));
-        assertEquals("Test 2", mConfig.getViewerString(1352021864));
-        assertEquals("Window %s is already added", mConfig.getViewerString(409412266));
-        assertNull(mConfig.getViewerString(1));
+        mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME });
+        assertEquals("My Test Log Message 1 %b", mConfig.getViewerString(1));
+        assertEquals("My Test Log Message 2 %b", mConfig.getViewerString(2));
+        assertEquals("My Test Log Message 3 %b", mConfig.getViewerString(3));
+        assertNull(mConfig.getViewerString(4));
+        assertNull(mConfig.getViewerString(5));
     }
 
     @Test
-    public void loadViewerConfig_invalidFile() {
-        mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist");
-        // No exception is thrown.
+    public void unloadViewerConfig() {
+        mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME, OTHER_TEST_GROUP_NAME });
+        mConfig.unloadViewerConfig(new String[] { TEST_GROUP_NAME });
         assertNull(mConfig.getViewerString(1));
+        assertNull(mConfig.getViewerString(2));
+        assertNull(mConfig.getViewerString(3));
+        assertEquals("My Test Log Message 4 %b", mConfig.getViewerString(4));
+        assertEquals("My Test Log Message 5 %b", mConfig.getViewerString(5));
+
+        mConfig.unloadViewerConfig(new String[] { OTHER_TEST_GROUP_NAME });
+        assertNull(mConfig.getViewerString(4));
+        assertNull(mConfig.getViewerString(5));
     }
 }
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
index 8433071..f385179 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java
@@ -54,7 +54,6 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -72,7 +71,6 @@
     private static final ComponentName TEST_ACTIVITY = new ComponentName(
             getInstrumentation().getTargetContext().getPackageName(),
             MainActivity.class.getName());
-    private static final long WAIT_TIME_MS = 3000L;
     private final Context mContext = getInstrumentation().getTargetContext();
     private final InputMethodManager mInputMethodManager =
             mContext.getSystemService(InputMethodManager.class);
@@ -111,7 +109,6 @@
         assertPassengerImeHidden();
     }
 
-    @Ignore("b/350562427")
     @Test
     public void passengerShowImeNotAffectDriver() throws Exception {
         assertDriverImeHidden();
@@ -259,8 +256,6 @@
         float[] driverEditTextCenter = mActivity.getEditTextCenter();
         SystemUtil.runShellCommand(mUiAutomation, String.format("input tap %f %f",
                 driverEditTextCenter[0], driverEditTextCenter[1]));
-        // TODO(b/350562427): get rid of Thread.sleep().
-        Thread.sleep(WAIT_TIME_MS);
     }
 
     private void movePassengerDisplayToTop() throws Exception {
@@ -276,8 +271,6 @@
         final int passengerDisplayId = receivedBundle.getInt(KEY_DISPLAY_ID);
         SystemUtil.runShellCommand(mUiAutomation, String.format("input -d %d tap %f %f",
                 passengerDisplayId, passengerEditTextCenter[0], passengerEditTextCenter[1]));
-        // TODO(b/350562427): get rid of Thread.sleep().
-        Thread.sleep(WAIT_TIME_MS);
     }
 
     /**
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 669cddb..cbf2c2f 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -18,9 +18,9 @@
 
 #include <unordered_set>
 
-#include "android-base/logging.h"
-
 #include "ResourceUtils.h"
+#include "android-base/logging.h"
+#include "process/SymbolTable.h"
 #include "trace/TraceBuffer.h"
 #include "util/Util.h"
 #include "xml/XmlActionExecutor.h"
@@ -172,6 +172,58 @@
   return NameIsJavaClassName(el, attr, diag);
 }
 
+static bool UpdateConfigChangesIfNeeded(xml::Element* el, IAaptContext* context) {
+  xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+  if (attr == nullptr) {
+    return true;
+  }
+
+  if (attr->value != "allKnown" && attr->value.find("allKnown") != std::string::npos) {
+    context->GetDiagnostics()->Error(
+        android::DiagMessage(el->line_number)
+        << "If you want to declare 'allKnown' in attribute 'android:configChanges' in <" << el->name
+        << ">, " << attr->value << " is not allowed', allKnown has to be used "
+        << "by itself, for example: 'android:configChanges=allKnown', it cannot be combined with "
+        << "the other flags");
+    return false;
+  }
+
+  if (attr->value == "allKnown") {
+    SymbolTable* symbol_table = context->GetExternalSymbols();
+    const SymbolTable::Symbol* symbol =
+        symbol_table->FindByName(ResourceName("android", ResourceType::kAttr, "configChanges"));
+
+    if (symbol == nullptr) {
+      context->GetDiagnostics()->Error(
+          android::DiagMessage(el->line_number)
+          << "Cannot find symbol for android:configChanges with min sdk: "
+          << context->GetMinSdkVersion());
+    }
+
+    std::stringstream new_value;
+
+    const auto& symbols = symbol->attribute->symbols;
+    for (auto it = symbols.begin(); it != symbols.end(); ++it) {
+      // Skip 'resourcesUnused' which is the flag to fully disable activity restart specifically
+      // for games.
+      if (it->symbol.name.value().entry == "resourcesUnused") {
+        continue;
+      }
+      if (it != symbols.begin()) {
+        new_value << "|";
+      }
+      new_value << it->symbol.name.value().entry;
+    }
+    const auto& old_value = attr->value;
+    auto new_value_str = new_value.str();
+    context->GetDiagnostics()->Note(android::DiagMessage(el->line_number)
+                                    << "Updating value of 'android:configChanges' from "
+                                    << old_value << " to " << new_value_str);
+    attr->value = std::move(new_value_str);
+  }
+  return true;
+}
+
 static bool RequiredNameIsJavaPackage(xml::Element* el, android::SourcePathDiagnostics* diag) {
   xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name");
   if (attr == nullptr) {
@@ -382,8 +434,9 @@
   }
 }
 
-bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag) {
+bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context) {
   // First verify some options.
+  android::IDiagnostics* diag = context->GetDiagnostics();
   if (options_.rename_manifest_package) {
     if (!util::IsJavaPackageName(options_.rename_manifest_package.value())) {
       diag->Error(android::DiagMessage() << "invalid manifest package override '"
@@ -432,6 +485,8 @@
   // Common component actions.
   xml::XmlNodeAction component_action;
   component_action.Action(RequiredNameIsJavaClassName);
+  component_action.Action(
+      [context](xml::Element* el) -> bool { return UpdateConfigChangesIfNeeded(el, context); });
   component_action["intent-filter"] = intent_filter_action;
   component_action["preferred"] = intent_filter_action;
   component_action["meta-data"] = meta_data_action;
@@ -778,7 +833,7 @@
   }
 
   xml::XmlActionExecutor executor;
-  if (!BuildRules(&executor, context->GetDiagnostics())) {
+  if (!BuildRules(&executor, context)) {
     return false;
   }
 
diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h
index df0ece6..748a828 100644
--- a/tools/aapt2/link/ManifestFixer.h
+++ b/tools/aapt2/link/ManifestFixer.h
@@ -110,7 +110,7 @@
  private:
   DISALLOW_COPY_AND_ASSIGN(ManifestFixer);
 
-  bool BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag);
+  bool BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context);
 
   ManifestFixerOptions options_;
 };
diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp
index 3cfdf78..5008627 100644
--- a/tools/aapt2/link/ManifestFixer_test.cpp
+++ b/tools/aapt2/link/ManifestFixer_test.cpp
@@ -37,27 +37,30 @@
             .SetCompilationPackage("android")
             .SetPackageId(0x01)
             .SetNameManglerPolicy(NameManglerPolicy{"android"})
-            .AddSymbolSource(
-                test::StaticSymbolSourceBuilder()
-                    .AddSymbol(
-                        "android:attr/package", ResourceId(0x01010000),
-                        test::AttributeBuilder()
-                            .SetTypeMask(android::ResTable_map::TYPE_STRING)
-                            .Build())
-                    .AddSymbol(
-                        "android:attr/minSdkVersion", ResourceId(0x01010001),
-                        test::AttributeBuilder()
-                            .SetTypeMask(android::ResTable_map::TYPE_STRING |
-                                         android::ResTable_map::TYPE_INTEGER)
-                            .Build())
-                    .AddSymbol(
-                        "android:attr/targetSdkVersion", ResourceId(0x01010002),
-                        test::AttributeBuilder()
-                            .SetTypeMask(android::ResTable_map::TYPE_STRING |
-                                         android::ResTable_map::TYPE_INTEGER)
-                            .Build())
-                    .AddSymbol("android:string/str", ResourceId(0x01060000))
-                    .Build())
+            .AddSymbolSource(test::StaticSymbolSourceBuilder()
+                                 .AddSymbol("android:attr/package", ResourceId(0x01010000),
+                                            test::AttributeBuilder()
+                                                .SetTypeMask(android::ResTable_map::TYPE_STRING)
+                                                .Build())
+                                 .AddSymbol("android:attr/minSdkVersion", ResourceId(0x01010001),
+                                            test::AttributeBuilder()
+                                                .SetTypeMask(android::ResTable_map::TYPE_STRING |
+                                                             android::ResTable_map::TYPE_INTEGER)
+                                                .Build())
+                                 .AddSymbol("android:attr/targetSdkVersion", ResourceId(0x01010002),
+                                            test::AttributeBuilder()
+                                                .SetTypeMask(android::ResTable_map::TYPE_STRING |
+                                                             android::ResTable_map::TYPE_INTEGER)
+                                                .Build())
+                                 .AddSymbol("android:string/str", ResourceId(0x01060000))
+                                 .AddSymbol("android:attr/configChanges", ResourceId(0x01010003),
+                                            test::AttributeBuilder()
+                                                .AddItem("testConfigChange1", 1)
+                                                .AddItem("testConfigChange2", 2)
+                                                .AddItem("resourcesUnused", 4)
+                                                .SetTypeMask(android::ResTable_map::TYPE_STRING)
+                                                .Build())
+                                 .Build())
             .Build();
   }
 
@@ -1591,4 +1594,72 @@
     </manifest>)";
   EXPECT_THAT(Verify(input), NotNull());
 }
+
+TEST_F(ManifestFixerTest, AllKnownNotDeclaredProperly) {
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity"
+                  android:configChanges="allKnown|testConfigChange1">
+        </activity>
+      </application>
+    </manifest>)";
+  auto doc = Verify(input);
+  EXPECT_THAT(doc, IsNull());
+}
+
+TEST_F(ManifestFixerTest, ModifyAttributeValueForAllKnown) {
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity"
+                  android:configChanges="allKnown">
+        </activity>
+      </application>
+    </manifest>)";
+  auto doc = Verify(input);
+  EXPECT_THAT(doc, NotNull());
+
+  xml::Element* el;
+  xml::Attribute* attr;
+
+  el = doc->root.get();
+  ASSERT_THAT(el, NotNull());
+  el = el->FindChild({}, "application");
+  ASSERT_THAT(el, NotNull());
+  el = el->FindChild({}, "activity");
+  ASSERT_THAT(el, NotNull());
+
+  attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+  ASSERT_THAT(attr->value, "testConfigChange1|testConfigChange2");
+}
+
+TEST_F(ManifestFixerTest, DoNothingForOtherConfigChanges) {
+  std::string input = R"(
+    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android">
+      <application>
+        <activity android:name=".MainActivity"
+                  android:configChanges="testConfigChange2">
+        </activity>
+      </application>
+    </manifest>)";
+  auto doc = Verify(input);
+  EXPECT_THAT(doc, NotNull());
+
+  xml::Element* el;
+  xml::Attribute* attr;
+
+  el = doc->root.get();
+  ASSERT_THAT(el, NotNull());
+  el = el->FindChild({}, "application");
+  ASSERT_THAT(el, NotNull());
+  el = el->FindChild({}, "activity");
+  ASSERT_THAT(el, NotNull());
+
+  attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+  ASSERT_THAT(attr->value, "testConfigChange2");
+}
 }  // namespace aapt
diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp
index 02e4beae..8ae55b8 100644
--- a/tools/aapt2/util/Files.cpp
+++ b/tools/aapt2/util/Files.cpp
@@ -189,7 +189,7 @@
   base->append(part.data(), part.size());
 }
 
-std::string BuildPath(std::vector<const StringPiece>&& args) {
+std::string BuildPath(const std::vector<StringPiece>& args) {
   if (args.empty()) {
     return "";
   }
diff --git a/tools/aapt2/util/Files.h b/tools/aapt2/util/Files.h
index 42eeaf2..c1a42446 100644
--- a/tools/aapt2/util/Files.h
+++ b/tools/aapt2/util/Files.h
@@ -60,7 +60,7 @@
 void AppendPath(std::string* base, android::StringPiece part);
 
 // Concatenates the list of paths and separates each part with the directory separator.
-std::string BuildPath(std::vector<const android::StringPiece>&& args);
+std::string BuildPath(const std::vector<android::StringPiece>& args);
 
 // Makes all the directories in `path`. The last element in the path is interpreted as a directory.
 bool mkdirs(const std::string& path);