Merge "Add flag for Preference Service Consent dialog" into main
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 14e2387..b43905b 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -451,7 +451,7 @@
                     auto token = SurfaceComposerClient::getPhysicalDisplayToken(
                         event.header.displayId);
 
-                    auto firstDisplay = mBootAnimation->mDisplays.front();
+                    auto& firstDisplay = mBootAnimation->mDisplays.front();
                     if (token != firstDisplay.displayToken) {
                         // ignore hotplug of a secondary display
                         continue;
diff --git a/core/api/current.txt b/core/api/current.txt
index ea6d1a5..d4d4b24 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -37819,6 +37819,7 @@
     field public static final String ACTION_DISPLAY_SETTINGS = "android.settings.DISPLAY_SETTINGS";
     field public static final String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
     field @Deprecated public static final String ACTION_FINGERPRINT_ENROLL = "android.settings.FINGERPRINT_ENROLL";
+    field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_FIRST_DAY_OF_WEEK_SETTINGS = "android.settings.FIRST_DAY_OF_WEEK_SETTINGS";
     field public static final String ACTION_HARD_KEYBOARD_SETTINGS = "android.settings.HARD_KEYBOARD_SETTINGS";
     field public static final String ACTION_HOME_SETTINGS = "android.settings.HOME_SETTINGS";
     field public static final String ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS = "android.settings.IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS";
@@ -37839,6 +37840,7 @@
     field public static final String ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING = "android.settings.MANAGE_SUPERVISOR_RESTRICTED_SETTING";
     field public static final String ACTION_MANAGE_UNKNOWN_APP_SOURCES = "android.settings.MANAGE_UNKNOWN_APP_SOURCES";
     field public static final String ACTION_MANAGE_WRITE_SETTINGS = "android.settings.action.MANAGE_WRITE_SETTINGS";
+    field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_MEASUREMENT_SYSTEM_SETTINGS = "android.settings.MEASUREMENT_SYSTEM_SETTINGS";
     field public static final String ACTION_MEMORY_CARD_SETTINGS = "android.settings.MEMORY_CARD_SETTINGS";
     field public static final String ACTION_NETWORK_OPERATOR_SETTINGS = "android.settings.NETWORK_OPERATOR_SETTINGS";
     field public static final String ACTION_NFCSHARING_SETTINGS = "android.settings.NFCSHARING_SETTINGS";
@@ -37849,12 +37851,14 @@
     field public static final String ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS = "android.settings.NOTIFICATION_LISTENER_DETAIL_SETTINGS";
     field public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS";
     field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS";
+    field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_NUMBERING_SYSTEM_SETTINGS = "android.settings.NUMBERING_SYSTEM_SETTINGS";
     field public static final String ACTION_PRINT_SETTINGS = "android.settings.ACTION_PRINT_SETTINGS";
     field public static final String ACTION_PRIVACY_SETTINGS = "android.settings.PRIVACY_SETTINGS";
     field public static final String ACTION_PROCESS_WIFI_EASY_CONNECT_URI = "android.settings.PROCESS_WIFI_EASY_CONNECT_URI";
     field public static final String ACTION_QUICK_ACCESS_WALLET_SETTINGS = "android.settings.QUICK_ACCESS_WALLET_SETTINGS";
     field public static final String ACTION_QUICK_LAUNCH_SETTINGS = "android.settings.QUICK_LAUNCH_SETTINGS";
     field public static final String ACTION_REGIONAL_PREFERENCES_SETTINGS = "android.settings.REGIONAL_PREFERENCES_SETTINGS";
+    field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_REGION_SETTINGS = "android.settings.REGION_SETTINGS";
     field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
     field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
     field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
@@ -37870,6 +37874,7 @@
     field public static final String ACTION_SOUND_SETTINGS = "android.settings.SOUND_SETTINGS";
     field @Deprecated public static final String ACTION_STORAGE_VOLUME_ACCESS_SETTINGS = "android.settings.STORAGE_VOLUME_ACCESS_SETTINGS";
     field public static final String ACTION_SYNC_SETTINGS = "android.settings.SYNC_SETTINGS";
+    field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_TEMPERATURE_UNIT_SETTINGS = "android.settings.TEMPERATURE_UNIT_SETTINGS";
     field public static final String ACTION_USAGE_ACCESS_SETTINGS = "android.settings.USAGE_ACCESS_SETTINGS";
     field public static final String ACTION_USER_DICTIONARY_SETTINGS = "android.settings.USER_DICTIONARY_SETTINGS";
     field public static final String ACTION_VOICE_CONTROL_AIRPLANE_MODE = "android.settings.VOICE_CONTROL_AIRPLANE_MODE";
@@ -54043,6 +54048,7 @@
     field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000
     field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
     field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
+    field @FlaggedApi("com.android.window.flags.supports_drag_assistant_to_multiwindow") public static final int DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START = 16384; // 0x4000
     field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
     field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG = 8192; // 0x2000
     field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0e521c6..f9ef62f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -709,8 +709,8 @@
     field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images";
     field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video";
     field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected";
-    field @FlaggedApi("android.permission.flags.platform_oxygen_saturation_enabled") public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
-    field @FlaggedApi("android.permission.flags.platform_skin_temperature_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
+    field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
+    field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
     field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data";
     field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio";
     field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast";
@@ -1368,6 +1368,7 @@
     method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long, boolean);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean packageHasActiveAdmins(String);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException;
+    method @FlaggedApi("android.app.admin.flags.remove_managed_profile_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean removeManagedProfile();
     method @RequiresPermission(android.Manifest.permission.TRIGGER_LOST_MODE) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public boolean setActiveProfileOwner(@NonNull android.content.ComponentName, String) throws java.lang.IllegalArgumentException;
     method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS) public void setApplicationExemptions(@NonNull String, @NonNull java.util.Set<java.lang.Integer>) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -18794,6 +18795,10 @@
     method @Deprecated public void openFileChooser(android.webkit.ValueCallback<android.net.Uri>, String, String);
   }
 
+  public abstract static class WebChromeClient.FileChooserParams {
+    field @FlaggedApi("android.webkit.file_system_access") public static final long ENABLE_FILE_SYSTEM_ACCESS = 364980165L; // 0x15c127c5L
+  }
+
   public abstract class WebHistoryItem implements java.lang.Cloneable {
     method @Deprecated public abstract int getId();
   }
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index fd70f4f..8b37dbd 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2527,12 +2527,12 @@
 
     /** @hide Access to read oxygen saturation. */
     @SystemApi
-    @FlaggedApi(Flags.FLAG_PLATFORM_OXYGEN_SATURATION_ENABLED)
+    @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
     public static final String OPSTR_READ_OXYGEN_SATURATION = "android:read_oxygen_saturation";
 
     /** @hide Access to read skin temperature. */
     @SystemApi
-    @FlaggedApi(Flags.FLAG_PLATFORM_SKIN_TEMPERATURE_ENABLED)
+    @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
     public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature";
 
     /** @hide Access to ranging */
@@ -2616,8 +2616,8 @@
             OP_POST_NOTIFICATION,
             // Health
             Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
-            Flags.platformSkinTemperatureEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
-            Flags.platformOxygenSaturationEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+            Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
+            Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
     };
 
     /**
@@ -3132,7 +3132,7 @@
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
         new AppOpInfo.Builder(OP_READ_SKIN_TEMPERATURE, OPSTR_READ_SKIN_TEMPERATURE,
             "READ_SKIN_TEMPERATURE").setPermission(
-                Flags.platformSkinTemperatureEnabled()
+                Flags.replaceBodySensorPermissionEnabled()
                     ? HealthPermissions.READ_SKIN_TEMPERATURE : null)
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
         new AppOpInfo.Builder(OP_RANGING, OPSTR_RANGING, "RANGING")
@@ -3141,7 +3141,7 @@
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
         new AppOpInfo.Builder(OP_READ_OXYGEN_SATURATION, OPSTR_READ_OXYGEN_SATURATION,
             "READ_OXYGEN_SATURATION").setPermission(
-                Flags.platformOxygenSaturationEnabled()
+                Flags.replaceBodySensorPermissionEnabled()
                     ? HealthPermissions.READ_OXYGEN_SATURATION : null)
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
     };
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 102540c..707ba34 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -55,6 +55,7 @@
 import static android.Manifest.permission.SET_TIME_ZONE;
 import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
 import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED;
+import static android.app.admin.flags.Flags.FLAG_REMOVE_MANAGED_PROFILE_ENABLED;
 import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
 import static android.app.admin.flags.Flags.onboardingConsentlessBugreports;
 import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
@@ -16962,6 +16963,30 @@
     }
 
     /**
+     * Removes a manged profile from the device only when called from a managed profile's context
+     *
+     * @param user UserHandle of the profile to be removed
+     * @return {@code true} when removal of managed profile was successful, {@code false} when
+     * removal was unsuccessful or throws IllegalArgumentException when provided user was not a
+     * managed profile
+     * @hide
+     */
+    @SystemApi
+    @UserHandleAware
+    @FlaggedApi(FLAG_REMOVE_MANAGED_PROFILE_ENABLED)
+    @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)
+    public boolean removeManagedProfile() {
+        if (mService == null) {
+            throw new IllegalStateException("Could not find DevicePolicyManagerService");
+        }
+        try {
+            return mService.removeManagedProfile(myUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Called when a managed profile has been provisioned.
      *
      * @throws SecurityException if the caller does not hold
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index a4e2b8f..ba97edb 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -567,6 +567,8 @@
 
     void finalizeWorkProfileProvisioning(in UserHandle managedProfileUser, in Account migratedAccount);
 
+    boolean removeManagedProfile(int userId);
+
     void setDeviceOwnerType(in ComponentName admin, in int deviceOwnerType);
     int getDeviceOwnerType(in ComponentName admin);
 
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index be24bfa..5f868be 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -343,13 +343,20 @@
 }
 
 flag {
-  name: "user_provisioning_same_state"
-  namespace: "enterprise"
-  description: "Handle exceptions while setting same provisioning state."
-  bug: "326441417"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
+    name: "user_provisioning_same_state"
+    namespace: "enterprise"
+    description: "Handle exceptions while setting same provisioning state."
+    bug: "326441417"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "remove_managed_profile_enabled"
+    namespace: "enterprise"
+    description: "API that removes a given managed profile."
+    bug: "372652841"
 }
 
 flag {
diff --git a/core/java/android/app/wallpaper/WallpaperDescription.java b/core/java/android/app/wallpaper/WallpaperDescription.java
index 8ffda72..4a142bb 100644
--- a/core/java/android/app/wallpaper/WallpaperDescription.java
+++ b/core/java/android/app/wallpaper/WallpaperDescription.java
@@ -113,7 +113,7 @@
     /** @return the description for this wallpaper */
     @NonNull
     public List<CharSequence> getDescription() {
-        return new ArrayList<>();
+        return mDescription;
     }
 
     /** @return the {@link Uri} for the action associated with the wallpaper, or {@code null} if not
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index b4b5a7f..9ba5a35 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -359,3 +359,10 @@
     bug: "377474232"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "support_minor_versions_in_minsdkversion"
+    namespace: "package_manager_service"
+    description: "Block app installations that specify an incompatible minor SDK version"
+    bug: "377474232"
+}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 96f6ad1..71b60cf 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -31,6 +31,7 @@
 import static com.android.hardware.input.Flags.touchpadThreeFingerTapShortcut;
 import static com.android.hardware.input.Flags.touchpadVisualizer;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
 import static com.android.input.flags.Flags.enableInputFilterRustImpl;
 import static com.android.input.flags.Flags.keyboardRepeatKeys;
@@ -1147,4 +1148,13 @@
     public static boolean isCustomizableInputGesturesFeatureFlagEnabled() {
         return enableCustomizableInputGestures() && useKeyGestureEventHandler();
     }
+
+    /**
+     * Whether multi-key gestures are supported using {@code KeyGestureEventHandler}
+     *
+     * @hide
+     */
+    public static boolean doesKeyGestureEventHandlerSupportMultiKeyGestures() {
+        return useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures();
+    }
 }
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 455b469..33040be 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -284,7 +284,7 @@
     is_fixed_read_only: true
     is_exported: true
     namespace: "android_health_services"
-    description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
+    description: "Enables replacement of BODY_SENSORS/BODY_SENSORS_BACKGROUND permissions with granular health permissions READ_HEART_RATE, READ_SKIN_TEMPERATURE, READ_OXYGEN_SATURATION, and READ_HEALTH_DATA_IN_BACKGROUND"
     bug: "364638912"
 }
 
@@ -300,24 +300,6 @@
 }
 
 flag {
-    name: "platform_skin_temperature_enabled"
-    is_fixed_read_only: true
-    is_exported: true
-    namespace: "android_health_services"
-    description: "This fixed read-only flag is used to enable platform support for Skin Temperature."
-    bug: "369872443"
-}
-
-flag {
-    name: "platform_oxygen_saturation_enabled"
-    is_fixed_read_only: true
-    is_exported: true
-    namespace: "android_health_services"
-    description: "This fixed read-only flag is used to enable platform support for Oxygen Saturation (SpO2)."
-    bug: "369873227"
-}
-
-flag {
     name: "allow_host_permission_dialogs_on_virtual_devices"
     is_exported: true
     namespace: "permissions"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ef35171..d2a20b6 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1217,6 +1217,69 @@
             "android.settings.REGIONAL_PREFERENCES_SETTINGS";
 
     /**
+     * Activity Action: Show screen for allowing the region configuration.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REGION_SETTINGS =
+            "android.settings.REGION_SETTINGS";
+
+    /**
+     * Activity Action: Show first day of week configuration settings.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_FIRST_DAY_OF_WEEK_SETTINGS =
+            "android.settings.FIRST_DAY_OF_WEEK_SETTINGS";
+
+    /**
+     * Activity Action: Show temperature unit configuration settings.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_TEMPERATURE_UNIT_SETTINGS =
+            "android.settings.TEMPERATURE_UNIT_SETTINGS";
+
+    /**
+     * Activity Action: Show numbering system configuration settings.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_NUMBERING_SYSTEM_SETTINGS =
+            "android.settings.NUMBERING_SYSTEM_SETTINGS";
+
+    /**
+     * Activity Action: Show measurement system configuration settings.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_MEASUREMENT_SYSTEM_SETTINGS =
+            "android.settings.MEASUREMENT_SYSTEM_SETTINGS";
+
+    /**
      * Activity Action: Show settings to allow configuration of lockscreen.
      * <p>
      * In some cases, a matching Activity may not exist, so ensure you
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index 4c63673..fff5363 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -63,3 +63,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "system_regional_preferences_api_enabled"
+    is_exported: true
+    namespace: "globalintl"
+    description: "Feature flag for regional preferences APIs"
+    bug: "370379000"
+}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 0241e94..a1a9fc6 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1786,7 +1786,12 @@
      *         {@code getWindowManager()} or {@code getSystemService(Context.WINDOW_SERVICE)}), the
      *         returned metrics provide the size of the current app window. As a result, in
      *         multi-window mode, the returned size can be smaller than the size of the device
-     *         screen.
+     *         screen. System decoration handling may vary depending on API level:
+     *         <ul>
+     *             <li>API level 35 and above, the window size will be returned.
+     *             <li>API level 34 and below, the window size minus system decoration areas and
+     *                 display cutout is returned.
+     *         </ul>
      *     <li>If metrics are requested from a non-activity context (for example, the application
      *         context, where the WindowManager is accessed by
      *         {@code getApplicationContext().getSystemService(Context.WINDOW_SERVICE)}), the
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 9714896..2d2f79d 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -23,6 +23,7 @@
 import android.annotation.UiThread;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.inputmethod.InputMethodDebug;
@@ -150,6 +151,17 @@
         if (!mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
             return InputMethodManager.DISPATCH_NOT_HANDLED;
         }
+        if (Flags.refactorInsetsController() && event instanceof KeyEvent keyEvent
+                && keyEvent.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+            final var insetsController = mViewRootImpl.getInsetsController();
+            if (insetsController.getAnimationType(WindowInsets.Type.ime())
+                    == InsetsController.ANIMATION_TYPE_HIDE
+                    || insetsController.isPredictiveBackImeHideAnimInProgress()) {
+                // if there is an ongoing hide animation, the back event should not be dispatched
+                // to the IME.
+                return InputMethodManager.DISPATCH_NOT_HANDLED;
+            }
+        }
         final InputMethodManager imm =
                 mViewRootImpl.mContext.getSystemService(InputMethodManager.class);
         if (imm == null) {
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 26ca813..b0813f3 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1910,7 +1910,8 @@
         mImeSourceConsumer.onWindowFocusLost();
     }
 
-    @VisibleForTesting
+    /** Returns the current {@link AnimationType} of an {@link InsetsType}. */
+    @VisibleForTesting(visibility = PACKAGE)
     public @AnimationType int getAnimationType(@InsetsType int type) {
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
             InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0b61c94..c71bf4b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -65,6 +65,7 @@
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
 import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
+import static com.android.window.flags.Flags.FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW;
 
 import static java.lang.Math.max;
 
@@ -5552,10 +5553,11 @@
 
     /**
      * Flag indicating that this drag will result in the caller activity's task to be hidden for the
-     * duration of the drag, this means that the source activity will not receive drag events for
-     * the current drag gesture. Only the current voice interaction service may use this flag.
-     * @hide
+     * duration of the drag, which means that the source activity will not receive drag events for
+     * the current drag gesture. Only the current
+     * {@link android.service.voice.VoiceInteractionService} may use this flag.
      */
+    @FlaggedApi(FLAG_SUPPORTS_DRAG_ASSISTANT_TO_MULTIWINDOW)
     public static final int DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START = 1 << 14;
 
     /**
diff --git a/core/java/android/view/WindowMetrics.java b/core/java/android/view/WindowMetrics.java
index 8bcc9de..12af692 100644
--- a/core/java/android/view/WindowMetrics.java
+++ b/core/java/android/view/WindowMetrics.java
@@ -107,8 +107,8 @@
      * and display cutout areas depending on the calling context and target SDK level. Please refer
      * to {@link Display#getSize(Point)} for details.
      * <p>
-     * The value reported by {@link Display#getSize(Point)} excluding system decoration areas can be
-     * obtained by using:
+     * The following code snippet shows how to get the bounds excluding navigation bars and display
+     * cutout:
      * <pre class="prettyprint">
      * final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
      * // Gets all excluding insets
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index 877fa74..1baf3b8 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -20,6 +20,8 @@
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.graphics.Bitmap;
@@ -552,6 +554,23 @@
      * Parameters used in the {@link #onShowFileChooser} method.
      */
     public static abstract class FileChooserParams {
+        /**
+         * Enable File System Access for webview.
+         *
+         * File System Access JS APIs window.showOpenFileChooser(), showDirectoryChooser(), and
+         * showSaveFilePicker() will invoke #onFileChooser(). Additional MODE_OPEN_FOLDER will be
+         * returned by #getMode(), #getIntent() will return ACTION_OPEN_DOCUMENT,
+         * ACTION_OPEN_DOCUMENT_TREE, and ACTION_CREATE_DOCUMENT rather than the current
+         * ACTION_GET_CONTENT, and new function #getPermissionMode() will be available.
+         *
+         * @hide
+         */
+        @ChangeId
+        @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM)
+        @FlaggedApi(android.webkit.Flags.FLAG_FILE_SYSTEM_ACCESS)
+        @SystemApi
+        public static final long ENABLE_FILE_SYSTEM_ACCESS = 364980165L;
+
         /** @hide */
         @IntDef(prefix = { "MODE_" }, value = {
             MODE_OPEN,
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index d9de38a..4f924a82 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -160,12 +160,21 @@
 }
 
 flag {
-  name: "delegate_unhandled_drags"
-  is_exported: true
-  namespace: "multitasking"
-  description: "Enables delegating unhandled drags to SystemUI"
-  bug: "320797628"
-  is_fixed_read_only: true
+    name: "delegate_unhandled_drags"
+    is_exported: true
+    namespace: "multitasking"
+    description: "Enables delegating unhandled drags to SystemUI"
+    bug: "320797628"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "supports_drag_assistant_to_multiwindow"
+    is_exported: true
+    namespace: "multitasking"
+    description: "Enables support for dragging the assistant into multiwindow"
+    bug: "371206207"
+    is_fixed_read_only: true
 }
 
 flag {
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
index 56f633f..ca1fc0a 100644
--- a/core/java/com/android/internal/app/AppLocaleCollector.java
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -41,7 +41,7 @@
 import java.util.stream.Collectors;
 
 /** The Locale data collector for per-app language. */
-public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class AppLocaleCollector implements LocaleCollectorBase {
     private static final String TAG = AppLocaleCollector.class.getSimpleName();
     private final Context mContext;
     private final String mAppPackageName;
@@ -167,8 +167,8 @@
     }
 
     @Override
-    public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
-        HashSet<String> langTagsToIgnore = new HashSet<>();
+    public Set<String> getIgnoredLocaleList(boolean translatedOnly) {
+        Set<String> langTagsToIgnore = new HashSet<>();
 
         if (mAppCurrentLocale != null) {
             langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
diff --git a/core/java/com/android/internal/app/LocaleCollectorBase.java b/core/java/com/android/internal/app/LocaleCollectorBase.java
new file mode 100644
index 0000000..f839077
--- /dev/null
+++ b/core/java/com/android/internal/app/LocaleCollectorBase.java
@@ -0,0 +1,36 @@
+/**
+ * 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.internal.app;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The interface which provides the locale list.
+ */
+public interface LocaleCollectorBase {
+
+    /** Gets the ignored locale list. */
+    Set<String> getIgnoredLocaleList(boolean translatedOnly);
+
+    /** Gets the supported locale list. */
+    Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
+            boolean translatedOnly, boolean isForCountryMode);
+
+    /** Indicates if the class work for specific package. */
+    boolean hasSpecificPackageName();
+}
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index ef4acd1..ffffefa 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -44,7 +44,10 @@
  * <p>It shows suggestions at the top, then the rest of the locales.
  * Allows the user to search for locales using both their native name and their name in the
  * default locale.</p>
+ *
+ * @deprecated use SettingLib's widget instead of customized UIs.
  */
+@Deprecated
 public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
     private static final String TAG = LocalePickerWithRegion.class.getSimpleName();
     private static final String PARENT_FRAGMENT_NAME = "localeListEditor";
@@ -78,21 +81,6 @@
         default void onParentLocaleSelected(LocaleStore.LocaleInfo locale) {}
     }
 
-    /**
-     * The interface which provides the locale list.
-     */
-    interface LocaleCollectorBase {
-        /** Gets the ignored locale list. */
-        HashSet<String> getIgnoredLocaleList(boolean translatedOnly);
-
-        /** Gets the supported locale list. */
-        Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
-                boolean translatedOnly, boolean isForCountryMode);
-
-        /** Indicates if the class work for specific package. */
-        boolean hasSpecificPackageName();
-    }
-
     private static LocalePickerWithRegion createNumberingSystemPicker(
             LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
             boolean translatedOnly, OnActionExpandListener onActionExpandListener,
diff --git a/core/java/com/android/internal/app/SystemLocaleCollector.java b/core/java/com/android/internal/app/SystemLocaleCollector.java
index 416f510..c7931cb 100644
--- a/core/java/com/android/internal/app/SystemLocaleCollector.java
+++ b/core/java/com/android/internal/app/SystemLocaleCollector.java
@@ -24,7 +24,7 @@
 import java.util.Set;
 
 /** The Locale data collector for System language. */
-class SystemLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class SystemLocaleCollector implements LocaleCollectorBase {
     private final Context mContext;
     private LocaleList mExplicitLocales;
 
@@ -32,14 +32,14 @@
         this(context, null);
     }
 
-    SystemLocaleCollector(Context context, LocaleList explicitLocales) {
+    public SystemLocaleCollector(Context context, LocaleList explicitLocales) {
         mContext = context;
         mExplicitLocales = explicitLocales;
     }
 
     @Override
-    public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
-        HashSet<String> ignoreList = new HashSet<>();
+    public Set<String> getIgnoredLocaleList(boolean translatedOnly) {
+        Set<String> ignoreList = new HashSet<>();
         if (!translatedOnly) {
             final LocaleList userLocales = LocalePicker.getLocales();
             final String[] langTags = userLocales.toLanguageTags().split(",");
diff --git a/core/res/res/values/stoppable_fgs_system_apps.xml b/core/res/res/values/stoppable_fgs_system_apps.xml
index 165ff61..06843f4 100644
--- a/core/res/res/values/stoppable_fgs_system_apps.xml
+++ b/core/res/res/values/stoppable_fgs_system_apps.xml
@@ -19,6 +19,7 @@
 <resources>
     <!-- A list of system apps whose FGS can be stopped in the task manager. -->
     <string-array translatable="false" name="stoppable_fgs_system_apps">
+        <item>com.android.virtualization.terminal</item>
     </string-array>
     <!-- stoppable_fgs_system_apps which is supposed to be overridden by vendor -->
     <string-array translatable="false" name="vendor_stoppable_fgs_system_apps">
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
index 35b2375..cae5d8e 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientParcelableChecker.java
@@ -96,7 +96,7 @@
             }
             if (WRITE_PARCELABLE.matches(tree, state)) {
                 return buildDescription(tree)
-                        .setMessage("Recommended to use 'writeTypedObject()' to improve "
+                        .setMessage("Recommended to use 'item.writeToParcel()' to improve "
                                 + "efficiency; saves overhead of Parcelable class name")
                         .build();
             }
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index b2ac640..636e3cf 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -32,7 +32,6 @@
             android:name=".desktopmode.DesktopWallpaperActivity"
             android:excludeFromRecents="true"
             android:launchMode="singleInstance"
-            android:showForAllUsers="true"
             android:theme="@style/DesktopWallpaperTheme" />
 
         <activity
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 0200e18..c99d9ba8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -188,6 +188,8 @@
      */
     private boolean mIsFirstReachabilityEducationRunning;
 
+    private boolean mIsInDesktopMode;
+
     @NonNull
     private final CompatUIStatusManager mCompatUIStatusManager;
 
@@ -253,18 +255,19 @@
         if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) {
             mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
         }
-
-        if (taskInfo != null && taskListener != null) {
-            updateActiveTaskInfo(taskInfo);
-        }
-
-        // We close all the Compat UI educations in case we're in desktop mode.
-        if (taskInfo.configuration == null || taskListener == null
-                || isInDesktopMode(taskInfo.displayId)) {
+        mIsInDesktopMode = isInDesktopMode(taskInfo);
+        // We close all the Compat UI educations in case TaskInfo has no configuration or
+        // TaskListener or in desktop mode.
+        if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) {
             // Null token means the current foreground activity is not in compatibility mode.
             removeLayouts(taskInfo.taskId);
             return;
         }
+        if (taskInfo != null && taskListener != null) {
+            updateActiveTaskInfo(taskInfo);
+        }
+
+
         // We're showing the first reachability education so we ignore incoming TaskInfo
         // until the education flow has completed or we double tap. The double-tap
         // basically cancel all the onboarding flow. We don't have to ignore events in case
@@ -443,7 +446,7 @@
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId);
         if (layout != null) {
-            if (layout.needsToBeRecreated(taskInfo, taskListener)) {
+            if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
                 mActiveCompatLayouts.remove(taskInfo.taskId);
                 layout.release();
             } else {
@@ -456,7 +459,10 @@
                 return;
             }
         }
-
+        if (mIsInDesktopMode) {
+            // Return if in desktop mode.
+            return;
+        }
         // Create a new UI layout.
         final Context context = getOrCreateDisplayContext(taskInfo.displayId);
         if (context == null) {
@@ -494,7 +500,8 @@
     private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (mActiveLetterboxEduLayout != null) {
-            if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+            if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)
+                    || mIsInDesktopMode) {
                 mActiveLetterboxEduLayout.release();
                 mActiveLetterboxEduLayout = null;
             } else {
@@ -507,6 +514,10 @@
                 return;
             }
         }
+        if (mIsInDesktopMode) {
+            // Return if in desktop mode.
+            return;
+        }
         // Create a new UI layout.
         final Context context = getOrCreateDisplayContext(taskInfo.displayId);
         if (context == null) {
@@ -541,7 +552,7 @@
         RestartDialogWindowManager layout =
                 mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
         if (layout != null) {
-            if (layout.needsToBeRecreated(taskInfo, taskListener)) {
+            if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
                 mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
                 layout.release();
             } else {
@@ -556,6 +567,10 @@
                 return;
             }
         }
+        if (mIsInDesktopMode) {
+            // Return if in desktop mode.
+            return;
+        }
         // Create a new UI layout.
         final Context context = getOrCreateDisplayContext(taskInfo.displayId);
         if (context == null) {
@@ -594,7 +609,8 @@
     private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (mActiveReachabilityEduLayout != null) {
-            if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) {
+            if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)
+                    || mIsInDesktopMode) {
                 mActiveReachabilityEduLayout.release();
                 mActiveReachabilityEduLayout = null;
             } else {
@@ -608,6 +624,10 @@
                 return;
             }
         }
+        if (mIsInDesktopMode) {
+            // Return if in desktop mode.
+            return;
+        }
         // Create a new UI layout.
         final Context context = getOrCreateDisplayContext(taskInfo.displayId);
         if (context == null) {
@@ -647,7 +667,8 @@
     private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo,
             @Nullable ShellTaskOrganizer.TaskListener taskListener) {
         if (mUserAspectRatioSettingsLayout != null) {
-            if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) {
+            if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)
+                    || mIsInDesktopMode) {
                 mUserAspectRatioSettingsLayout.release();
                 mUserAspectRatioSettingsLayout = null;
             } else {
@@ -660,7 +681,10 @@
                 return;
             }
         }
-
+        if (mIsInDesktopMode) {
+            // Return if in desktop mode.
+            return;
+        }
         // Create a new UI layout.
         final Context context = getOrCreateDisplayContext(taskInfo.displayId);
         if (context == null) {
@@ -840,8 +864,8 @@
         boolean mHasShownUserAspectRatioSettingsButtonHint;
     }
 
-    private boolean isInDesktopMode(int displayId) {
-        return Flags.skipCompatUiEducationInDesktopMode()
-                && mInDesktopModePredicate.test(displayId);
+    private boolean isInDesktopMode(@Nullable TaskInfo taskInfo) {
+        return taskInfo != null && Flags.skipCompatUiEducationInDesktopMode()
+                && mInDesktopModePredicate.test(taskInfo.displayId);
     }
 }
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 223038f..7446b88 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
@@ -42,6 +42,7 @@
 import android.os.Handler
 import android.os.IBinder
 import android.os.SystemProperties
+import android.os.UserHandle
 import android.util.Size
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.DragEvent
@@ -1174,7 +1175,11 @@
 
     private fun addWallpaperActivity(wct: WindowContainerTransaction) {
         logV("addWallpaperActivity")
-        val intent = Intent(context, DesktopWallpaperActivity::class.java)
+        val userHandle = UserHandle.of(userId)
+        val userContext =
+            context.createContextAsUser(userHandle, /* flags= */ 0)
+        val intent = Intent(userContext, DesktopWallpaperActivity::class.java)
+        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
         val options =
             ActivityOptions.makeBasic().apply {
                 launchWindowingMode = WINDOWING_MODE_FULLSCREEN
@@ -1182,11 +1187,13 @@
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
             }
         val pendingIntent =
-            PendingIntent.getActivity(
-                context,
-                /* requestCode = */ 0,
+            PendingIntent.getActivityAsUser(
+                userContext,
+                /* requestCode= */ 0,
                 intent,
-                PendingIntent.FLAG_IMMUTABLE
+                PendingIntent.FLAG_IMMUTABLE,
+                /* bundle= */ null,
+                userHandle
             )
         wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
index 1c2415c..e835b2f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
@@ -36,7 +36,6 @@
 class DesktopWallpaperActivity : Activity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate")
         super.onCreate(savedInstanceState)
         window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index f739d65..49cf8ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -63,6 +63,8 @@
             "Bubbles"),
     WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_COMPAT_UI),
+    WM_SHELL_APP_COMPAT(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM_APP_COMPAT),
     TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
 
     private final boolean mEnabled;
@@ -131,6 +133,7 @@
         private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
         private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode";
         private static final String TAG_WM_COMPAT_UI = "CompatUi";
+        private static final String TAG_WM_APP_COMPAT = "AppCompat";
 
         private static final boolean ENABLE_DEBUG = true;
         private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
diff --git a/media/java/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/PictureProfileHandle.aidl
new file mode 100644
index 0000000..5d14631
--- /dev/null
+++ b/media/java/android/media/quality/PictureProfileHandle.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.media.quality;
+
+parcelable PictureProfileHandle;
diff --git a/media/java/android/media/quality/PictureProfileHandle.java b/media/java/android/media/quality/PictureProfileHandle.java
new file mode 100644
index 0000000..2b1cda4
--- /dev/null
+++ b/media/java/android/media/quality/PictureProfileHandle.java
@@ -0,0 +1,75 @@
+/*
+ * 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 android.media.quality;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+// TODO(b/337330263): Expose as public API after API review
+/**
+  * A type-safe handle to a picture profile, which represents a collection of parameters used to
+  * configure picture processing hardware to enhance the quality of graphic buffers.
+  * @hide
+  */
+@FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+public final class PictureProfileHandle implements Parcelable {
+    private final long mId;
+
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    public PictureProfileHandle(long id) {
+        mId = id;
+    }
+
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    public long getId() {
+        return mId;
+    }
+
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mId);
+    }
+
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW)
+    @NonNull
+    public static final Creator<PictureProfileHandle> CREATOR =
+            new Creator<PictureProfileHandle>() {
+                @Override
+                public PictureProfileHandle createFromParcel(Parcel in) {
+                    return new PictureProfileHandle(in);
+                }
+
+                @Override
+                public PictureProfileHandle[] newArray(int size) {
+                    return new PictureProfileHandle[size];
+                }
+            };
+
+    private PictureProfileHandle(@NonNull Parcel in) {
+        mId = in.readLong();
+    }
+}
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 821ea21..bf419cc 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -74,16 +74,6 @@
 }
 
 flag {
-    name: "volume_panel_broadcast_fix"
-    namespace: "systemui"
-    description: "Make the volume panel's repository listen for the new ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED broadcast instead of ACTION_NOTIFICATION_POLICY_CHANGED"
-    bug: "347707024"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "volume_dialog_audio_sharing_fix"
     namespace: "cross_device_experiences"
     description: "Gates whether to show separate volume bars during audio sharing"
@@ -111,6 +101,14 @@
 }
 
 flag {
+    name: "write_system_preference_permission_enabled"
+    is_fixed_read_only: true
+    namespace: "android_settings"
+    description: "Enable WRITE_SYSTEM_PREFERENCE permission and appop"
+    bug: "375193223"
+}
+
+flag {
   name: "asha_profile_access_profile_enabled_true"
   namespace: "accessibility"
   description: "Changes the return value of HearingAidProfile.accessProfileEnabled() to true"
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index 7fdbcda..f446bb8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -93,33 +93,23 @@
                     IntentFilter().apply {
                         addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
                         addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
-                        if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+                        if (android.app.Flags.modesApi())
                             addAction(
-                                NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED)
+                                NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED
+                            )
                     },
                     /* broadcastPermission = */ null,
-                    /* scheduler = */ if (Flags.volumePanelBroadcastFix()) {
-                        backgroundHandler
-                    } else {
-                        null
-                    },
+                    /* scheduler = */ backgroundHandler,
                 )
 
                 awaitClose { context.unregisterReceiver(receiver) }
             }
-            .let {
-                if (Flags.volumePanelBroadcastFix()) {
-                    // Share the flow to avoid having multiple broadcasts.
-                    it.flowOn(backgroundCoroutineContext)
-                        .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
-                } else {
-                    it.shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
-                }
-            }
+            .flowOn(backgroundCoroutineContext)
+            .shareIn(started = SharingStarted.WhileSubscribed(), scope = scope)
     }
 
     override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
-        if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
+        if (android.app.Flags.modesApi())
             flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
                 // If available, get the value from extras to avoid a potential binder call.
                 it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY)
@@ -161,11 +151,13 @@
                     contentResolver.registerContentObserver(
                         Settings.Global.getUriFor(Settings.Global.ZEN_MODE),
                         /* notifyForDescendants= */ false,
-                        observer)
+                        observer,
+                    )
                     contentResolver.registerContentObserver(
                         Settings.Global.getUriFor(Settings.Global.ZEN_MODE_CONFIG_ETAG),
                         /* notifyForDescendants= */ false,
-                        observer)
+                        observer,
+                    )
 
                     awaitClose { contentResolver.unregisterContentObserver(observer) }
                 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index c136644..388af61 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -23,12 +23,10 @@
 import android.content.Intent
 import android.database.ContentObserver
 import android.os.Parcelable
-import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.provider.Settings.Global
 import androidx.test.filters.SmallTest
-import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
@@ -93,26 +91,7 @@
             )
     }
 
-    @DisableFlags(Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
-    @Test
-    fun consolidatedPolicyChanges_repositoryEmits_flagsOff() {
-        testScope.runTest {
-            val values = mutableListOf<NotificationManager.Policy?>()
-            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy1)
-            underTest.consolidatedNotificationPolicy
-                .onEach { values.add(it) }
-                .launchIn(backgroundScope)
-            runCurrent()
-
-            `when`(notificationManager.consolidatedNotificationPolicy).thenReturn(testPolicy2)
-            triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
-            runCurrent()
-
-            assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder()
-        }
-    }
-
-    @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @Test
     fun consolidatedPolicyChanges_repositoryEmits_flagsOn() {
         testScope.runTest {
@@ -131,7 +110,7 @@
         }
     }
 
-    @EnableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+    @EnableFlags(android.app.Flags.FLAG_MODES_API)
     @Test
     fun consolidatedPolicyChanges_repositoryEmitsFromExtras() {
         testScope.runTest {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 041cd15..c33d655 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -25,7 +25,6 @@
 import androidx.compose.ui.unit.round
 import androidx.compose.ui.util.fastCoerceIn
 import com.android.compose.animation.scene.content.Content
-import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
 import com.android.compose.nestedscroll.OnStopScope
 import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -36,11 +35,11 @@
 
 internal interface DraggableHandler {
     /**
-     * Start a drag with the given [pointersInfo] and [overSlop].
+     * Start a drag with the given [pointersDown] and [overSlop].
      *
      * The returned [DragController] should be used to continue or stop the drag.
      */
-    fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController
+    fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController
 }
 
 /**
@@ -95,7 +94,7 @@
      * Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f,
      * indicating that the transition should be intercepted.
      */
-    internal fun shouldImmediatelyIntercept(pointersInfo: PointersInfo?): Boolean {
+    internal fun shouldImmediatelyIntercept(pointersDown: PointersInfo.PointersDown?): Boolean {
         // We don't intercept the touch if we are not currently driving the transition.
         val dragController = dragController
         if (dragController?.isDrivingTransition != true) {
@@ -106,7 +105,7 @@
 
         // Only intercept the current transition if one of the 2 swipes results is also a transition
         // between the same pair of contents.
-        val swipes = computeSwipes(pointersInfo)
+        val swipes = computeSwipes(pointersDown)
         val fromContent = layoutImpl.content(swipeAnimation.currentContent)
         val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent)
         val currentScene = layoutImpl.state.currentScene
@@ -123,7 +122,10 @@
                 ))
     }
 
-    override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController {
+    override fun onDragStarted(
+        pointersDown: PointersInfo.PointersDown?,
+        overSlop: Float,
+    ): DragController {
         if (overSlop == 0f) {
             val oldDragController = dragController
             check(oldDragController != null && oldDragController.isDrivingTransition) {
@@ -148,7 +150,7 @@
             return updateDragController(swipes, swipeAnimation)
         }
 
-        val swipes = computeSwipes(pointersInfo)
+        val swipes = computeSwipes(pointersDown)
         val fromContent = layoutImpl.contentForUserActions()
 
         swipes.updateSwipesResults(fromContent)
@@ -194,11 +196,11 @@
         )
     }
 
-    private fun computeSwipes(pointersInfo: PointersInfo?): Swipes {
-        val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) }
+    private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes {
+        val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) }
         return Swipes(
-            upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource),
-            downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource),
+            upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource),
+            downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource),
         )
     }
 }
@@ -206,7 +208,7 @@
 private fun resolveSwipe(
     orientation: Orientation,
     isUpOrLeft: Boolean,
-    pointersInfo: PointersInfo?,
+    pointersDown: PointersInfo.PointersDown?,
     fromSource: SwipeSource.Resolved?,
 ): Swipe.Resolved {
     return Swipe.Resolved(
@@ -227,9 +229,9 @@
                     }
             },
         // If the number of pointers is not specified, 1 is assumed.
-        pointerCount = pointersInfo?.pointersDown ?: 1,
+        pointerCount = pointersDown?.count ?: 1,
         // Resolves the pointer type only if all pointers are of the same type.
-        pointersType = pointersInfo?.pointersDownByType?.keys?.singleOrNull(),
+        pointersType = pointersDown?.countByType?.keys?.singleOrNull(),
         fromSource = fromSource,
     )
 }
@@ -540,40 +542,15 @@
 
     val connection: PriorityNestedScrollConnection = nestedScrollConnection()
 
-    private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved {
-        return resolveSwipe(
-            orientation = draggableHandler.orientation,
-            isUpOrLeft = isUpOrLeft,
-            pointersInfo = pointersInfo,
-            fromSource =
-                pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) },
-        )
-    }
-
     private fun nestedScrollConnection(): PriorityNestedScrollConnection {
         // If we performed a long gesture before entering priority mode, we would have to avoid
         // moving on to the next scene.
         var canChangeScene = false
 
-        var lastPointersInfo: PointersInfo? = null
+        var lastPointersDown: PointersInfo.PointersDown? = null
 
-        fun hasNextScene(amount: Float): Boolean {
-            val transitionState = layoutState.transitionState
-            val scene = transitionState.currentScene
-            val fromScene = layoutImpl.scene(scene)
-            val resolvedSwipe =
-                when {
-                    amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo)
-                    amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo)
-                    else -> null
-                }
-            val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) }
-            if (nextScene != null) return true
-
-            if (transitionState !is TransitionState.Idle) return false
-
-            val overscrollSpec = layoutImpl.state.transitions.overscrollSpec(scene, orientation)
-            return overscrollSpec != null
+        fun shouldEnableSwipes(): Boolean {
+            return layoutImpl.contentForUserActions().shouldEnableSwipes(orientation)
         }
 
         var isIntercepting = false
@@ -581,14 +558,24 @@
         return PriorityNestedScrollConnection(
             orientation = orientation,
             canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ ->
-                val pointersInfo = pointersInfoOwner.pointersInfo()
+                val pointersDown: PointersInfo.PointersDown? =
+                    when (val info = pointersInfoOwner.pointersInfo()) {
+                        PointersInfo.MouseWheel -> {
+                            // Do not support mouse wheel interactions
+                            return@PriorityNestedScrollConnection false
+                        }
+
+                        is PointersInfo.PointersDown -> info
+                        null -> null
+                    }
+
                 canChangeScene =
                     if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
 
                 val canInterceptSwipeTransition =
                     canChangeScene &&
                         offsetAvailable != 0f &&
-                        draggableHandler.shouldImmediatelyIntercept(pointersInfo)
+                        draggableHandler.shouldImmediatelyIntercept(pointersDown)
                 if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
 
                 val threshold = layoutImpl.transitionInterceptionThreshold
@@ -599,11 +586,7 @@
                     return@PriorityNestedScrollConnection false
                 }
 
-                if (pointersInfo?.isMouseWheel == true) {
-                    // Do not support mouse wheel interactions
-                    return@PriorityNestedScrollConnection false
-                }
-                lastPointersInfo = pointersInfo
+                lastPointersDown = pointersDown
 
                 // If the current swipe transition is *not* closed to 0f or 1f, then we want the
                 // scroll events to intercept the current transition to continue the scene
@@ -622,28 +605,33 @@
                 val isZeroOffset =
                     if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
 
-                val pointersInfo = pointersInfoOwner.pointersInfo()
-                if (pointersInfo?.isMouseWheel == true) {
-                    // Do not support mouse wheel interactions
-                    return@PriorityNestedScrollConnection false
-                }
-                lastPointersInfo = pointersInfo
+                val pointersDown: PointersInfo.PointersDown? =
+                    when (val info = pointersInfoOwner.pointersInfo()) {
+                        PointersInfo.MouseWheel -> {
+                            // Do not support mouse wheel interactions
+                            return@PriorityNestedScrollConnection false
+                        }
+
+                        is PointersInfo.PointersDown -> info
+                        null -> null
+                    }
+                lastPointersDown = pointersDown
 
                 val canStart =
                     when (behavior) {
                         NestedScrollBehavior.EdgeNoPreview -> {
                             canChangeScene = isZeroOffset
-                            isZeroOffset && hasNextScene(offsetAvailable)
+                            isZeroOffset && shouldEnableSwipes()
                         }
 
                         NestedScrollBehavior.EdgeWithPreview -> {
                             canChangeScene = isZeroOffset
-                            hasNextScene(offsetAvailable)
+                            shouldEnableSwipes()
                         }
 
                         NestedScrollBehavior.EdgeAlways -> {
                             canChangeScene = true
-                            hasNextScene(offsetAvailable)
+                            shouldEnableSwipes()
                         }
                     }
 
@@ -664,14 +652,19 @@
                 // We could start an overscroll animation
                 canChangeScene = false
 
-                val pointersInfo = pointersInfoOwner.pointersInfo()
-                if (pointersInfo?.isMouseWheel == true) {
-                    // Do not support mouse wheel interactions
-                    return@PriorityNestedScrollConnection false
-                }
-                lastPointersInfo = pointersInfo
+                val pointersDown: PointersInfo.PointersDown? =
+                    when (val info = pointersInfoOwner.pointersInfo()) {
+                        PointersInfo.MouseWheel -> {
+                            // Do not support mouse wheel interactions
+                            return@PriorityNestedScrollConnection false
+                        }
 
-                val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
+                        is PointersInfo.PointersDown -> info
+                        null -> null
+                    }
+                lastPointersDown = pointersDown
+
+                val canStart = behavior.canStartOnPostFling && shouldEnableSwipes()
                 if (canStart) {
                     isIntercepting = false
                 }
@@ -679,11 +672,10 @@
                 canStart
             },
             onStart = { firstScroll ->
-                val pointersInfo = lastPointersInfo
                 scrollController(
                     dragController =
                         draggableHandler.onDragStarted(
-                            pointersInfo = pointersInfo,
+                            pointersDown = lastPointersDown,
                             overSlop = if (isIntercepting) 0f else firstScroll,
                         ),
                     canChangeScene = canChangeScene,
@@ -701,8 +693,7 @@
 ): ScrollController {
     return object : ScrollController {
         override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float {
-            val pointersInfo = pointersInfoOwner.pointersInfo()
-            if (pointersInfo?.isMouseWheel == true) {
+            if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) {
                 // Do not support mouse wheel interactions
                 return 0f
             }
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 ab2324a..1603267 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
@@ -80,8 +80,8 @@
 @Stable
 internal fun Modifier.multiPointerDraggable(
     orientation: Orientation,
-    startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
-    onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+    startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+    onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
     onFirstPointerDown: () -> Unit = {},
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
     dispatcher: NestedScrollDispatcher,
@@ -99,8 +99,9 @@
 
 private data class MultiPointerDraggableElement(
     private val orientation: Orientation,
-    private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
-    private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+    private val startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+    private val onDragStarted:
+        (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
     private val onFirstPointerDown: () -> Unit,
     private val swipeDetector: SwipeDetector,
     private val dispatcher: NestedScrollDispatcher,
@@ -126,8 +127,8 @@
 
 internal class MultiPointerDraggableNode(
     orientation: Orientation,
-    var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
-    var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+    var startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+    var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
     var onFirstPointerDown: () -> Unit,
     swipeDetector: SwipeDetector = DefaultSwipeDetector,
     private val dispatcher: NestedScrollDispatcher,
@@ -185,20 +186,27 @@
 
     private var lastPointerEvent: PointerEvent? = null
     private var startedPosition: Offset? = null
-    private var pointersDown: Int = 0
+    private var countPointersDown: Int = 0
 
     internal fun pointersInfo(): PointersInfo? {
-        val startedPosition = startedPosition
-        val lastPointerEvent = lastPointerEvent
-        if (startedPosition == null || lastPointerEvent == null) {
-            // This may be null, i.e. when the user uses TalkBack
-            return null
-        }
+        // This may be null, i.e. when the user uses TalkBack
+        val lastPointerEvent = lastPointerEvent ?: return null
 
-        return PointersInfo(
+        if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel
+
+        val startedPosition = startedPosition ?: return null
+
+        return PointersInfo.PointersDown(
             startedPosition = startedPosition,
-            pointersDown = pointersDown,
-            lastPointerEvent = lastPointerEvent,
+            count = countPointersDown,
+            countByType =
+                buildMap {
+                    lastPointerEvent.changes.fastForEach { change ->
+                        if (!change.pressed) return@fastForEach
+                        val newValue = (get(change.type) ?: 0) + 1
+                        put(change.type, newValue)
+                    }
+                },
         )
     }
 
@@ -218,11 +226,11 @@
 
                 val changes = pointerEvent.changes
                 lastPointerEvent = pointerEvent
-                pointersDown = changes.countDown()
+                countPointersDown = changes.countDown()
 
                 when {
                     // There are no more pointers down.
-                    pointersDown == 0 -> {
+                    countPointersDown == 0 -> {
                         startedPosition = null
 
                         // In case of multiple events with 0 pointers down (not pressed) we may have
@@ -290,8 +298,8 @@
                     detectDragGestures(
                         orientation = orientation,
                         startDragImmediately = startDragImmediately,
-                        onDragStart = { pointersInfo, overSlop ->
-                            onDragStarted(pointersInfo, overSlop)
+                        onDragStart = { pointersDown, overSlop ->
+                            onDragStarted(pointersDown, overSlop)
                         },
                         onDrag = { controller, amount ->
                             dispatchScrollEvents(
@@ -440,8 +448,8 @@
      */
     private suspend fun AwaitPointerEventScope.detectDragGestures(
         orientation: Orientation,
-        startDragImmediately: (pointersInfo: PointersInfo) -> Boolean,
-        onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController,
+        startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean,
+        onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController,
         onDrag: (controller: DragController, dragAmount: Float) -> Unit,
         onDragEnd: (controller: DragController) -> Unit,
         onDragCancel: (controller: DragController) -> Unit,
@@ -466,13 +474,14 @@
                 .first()
 
         var overSlop = 0f
-        var lastPointersInfo =
+        var lastPointersDown: PointersInfo.PointersDown =
             checkNotNull(pointersInfo()) {
                 "We should have pointers down, last event: $currentEvent"
             }
+                as PointersInfo.PointersDown
 
         val drag =
-            if (startDragImmediately(lastPointersInfo)) {
+            if (startDragImmediately(lastPointersDown)) {
                 consumablePointer.consume()
                 consumablePointer
             } else {
@@ -499,10 +508,11 @@
                             )
                     } ?: return
 
-                lastPointersInfo =
+                lastPointersDown =
                     checkNotNull(pointersInfo()) {
                         "We should have pointers down, last event: $currentEvent"
                     }
+                        as PointersInfo.PointersDown
                 // Make sure that overSlop is not 0f. This can happen when the user drags by exactly
                 // the touch slop. However, the overSlop we pass to onDragStarted() is used to
                 // compute the direction we are dragging in, so overSlop should never be 0f unless
@@ -516,7 +526,7 @@
                 drag
             }
 
-        val controller = onDragStart(lastPointersInfo, overSlop)
+        val controller = onDragStart(lastPointersDown, overSlop)
 
         val successful: Boolean
         try {
@@ -666,48 +676,31 @@
     fun pointersInfo(): PointersInfo?
 }
 
-/**
- * Holds information about pointer interactions within a composable.
- *
- * This class stores details such as the starting position of a gesture, the number of pointers
- * down, and whether the last pointer event was a mouse wheel scroll.
- *
- * @param startedPosition The starting position of the gesture. This is the position where the first
- *   pointer touched the screen, not necessarily the point where dragging begins. This may be
- *   different from the initial touch position if a child composable intercepts the gesture before
- *   this one.
- * @param pointersDown The number of pointers currently down.
- * @param isMouseWheel Indicates whether the last pointer event was a mouse wheel scroll.
- * @param pointersDownByType Provide a map of pointer types to the count of pointers of that type
- *   currently down/pressed.
- */
-internal data class PointersInfo(
-    val startedPosition: Offset,
-    val pointersDown: Int,
-    val isMouseWheel: Boolean,
-    val pointersDownByType: Map<PointerType, Int>,
-) {
-    init {
-        check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" }
+internal sealed interface PointersInfo {
+    /**
+     * Holds information about pointer interactions within a composable.
+     *
+     * This class stores details such as the starting position of a gesture, the number of pointers
+     * down, and whether the last pointer event was a mouse wheel scroll.
+     *
+     * @param startedPosition The starting position of the gesture. This is the position where the
+     *   first pointer touched the screen, not necessarily the point where dragging begins. This may
+     *   be different from the initial touch position if a child composable intercepts the gesture
+     *   before this one.
+     * @param count The number of pointers currently down.
+     * @param countByType Provide a map of pointer types to the count of pointers of that type
+     *   currently down/pressed.
+     */
+    data class PointersDown(
+        val startedPosition: Offset,
+        val count: Int,
+        val countByType: Map<PointerType, Int>,
+    ) : PointersInfo {
+        init {
+            check(count > 0) { "We should have at least 1 pointer down, $count instead" }
+        }
     }
-}
 
-private fun PointersInfo(
-    startedPosition: Offset,
-    pointersDown: Int,
-    lastPointerEvent: PointerEvent,
-): PointersInfo {
-    return PointersInfo(
-        startedPosition = startedPosition,
-        pointersDown = pointersDown,
-        isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll,
-        pointersDownByType =
-            buildMap {
-                lastPointerEvent.changes.fastForEach { change ->
-                    if (!change.pressed) return@fastForEach
-                    val newValue = (get(change.type) ?: 0) + 1
-                    put(change.type, newValue)
-                }
-            },
-    )
+    /** Indicates whether the last pointer event was a mouse wheel scroll. */
+    data object MouseWheel : PointersInfo
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 61332b6..72b29ee 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -25,17 +25,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.util.fastAll
 import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastFilter
 import androidx.compose.ui.util.fastForEach
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.transformation.SharedElementTransformation
-import com.android.compose.animation.scene.transition.link.LinkedTransition
-import com.android.compose.animation.scene.transition.link.StateLink
 import kotlin.math.absoluteValue
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 /**
@@ -236,7 +232,6 @@
     canShowOverlay: (OverlayKey) -> Boolean = { true },
     canHideOverlay: (OverlayKey) -> Boolean = { true },
     canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
-    stateLinks: List<StateLink> = emptyList(),
 ): MutableSceneTransitionLayoutState {
     return MutableSceneTransitionLayoutStateImpl(
         initialScene,
@@ -246,7 +241,6 @@
         canShowOverlay,
         canHideOverlay,
         canReplaceOverlay,
-        stateLinks,
     )
 }
 
@@ -258,10 +252,7 @@
     internal val canChangeScene: (SceneKey) -> Boolean = { true },
     internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
     internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
-    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
-        true
-    },
-    private val stateLinks: List<StateLink> = emptyList(),
+    internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
 ) : MutableSceneTransitionLayoutState {
     private val creationThread: Thread = Thread.currentThread()
 
@@ -364,20 +355,11 @@
         checkThread()
 
         try {
-            // Keep a reference to the previous transition (if any).
-            val previousTransition = currentTransition
-
             // Start the transition.
             startTransitionInternal(transition, chain)
 
-            // Handle transition links.
-            previousTransition?.let { cancelActiveTransitionLinks(it) }
-            if (stateLinks.isNotEmpty()) {
-                coroutineScope { setupTransitionLinks(transition) }
-            }
-
             // Run the transition until it is finished.
-            transition.run()
+            transition.runInternal()
         } finally {
             finishTransition(transition)
         }
@@ -471,42 +453,6 @@
         )
     }
 
-    private fun cancelActiveTransitionLinks(transition: TransitionState.Transition) {
-        transition.activeTransitionLinks.forEach { (link, linkedTransition) ->
-            link.target.finishTransition(linkedTransition)
-        }
-        transition.activeTransitionLinks.clear()
-    }
-
-    private fun CoroutineScope.setupTransitionLinks(transition: TransitionState.Transition) {
-        stateLinks.fastForEach { stateLink ->
-            val matchingLinks =
-                stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) }
-            if (matchingLinks.isEmpty()) return@fastForEach
-            if (matchingLinks.size > 1) error("More than one link matched.")
-
-            val targetCurrentScene = stateLink.target.transitionState.currentScene
-            val matchingLink = matchingLinks[0]
-
-            if (!matchingLink.targetIsInValidState(targetCurrentScene)) return@fastForEach
-
-            val linkedTransition =
-                LinkedTransition(
-                    originalTransition = transition,
-                    fromScene = targetCurrentScene,
-                    toScene = matchingLink.targetTo,
-                    key = matchingLink.targetTransitionKey,
-                )
-
-            // Start with UNDISPATCHED so that startTransition is called directly and the new linked
-            // transition is observable directly.
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                stateLink.target.startTransition(linkedTransition)
-            }
-            transition.activeTransitionLinks[stateLink] = linkedTransition
-        }
-    }
-
     /**
      * Notify that [transition] was finished and that it settled to its
      * [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was
@@ -535,9 +481,6 @@
         // Mark this transition as finished.
         finishedTransitions.add(transition)
 
-        // Finish all linked transitions.
-        finishActiveTransitionLinks(transition)
-
         // Keep a reference to the last transition, in case we remove all transitions and should
         // settle to Idle.
         val lastTransition = transitionStates.last()
@@ -584,13 +527,6 @@
         transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
     }
 
-    private fun finishActiveTransitionLinks(transition: TransitionState.Transition) {
-        for ((link, linkedTransition) in transition.activeTransitionLinks) {
-            link.target.finishTransition(linkedTransition)
-        }
-        transition.activeTransitionLinks.clear()
-    }
-
     /**
      * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap
      * to the closest scene.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index ba5f414..a448ee4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -56,7 +56,7 @@
 }
 
 /** Whether swipe should be enabled in the given [orientation]. */
-private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
+internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
     if (userActions.isEmpty()) {
         return false
     }
@@ -200,10 +200,10 @@
 
     override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
 
-    private fun startDragImmediately(pointersInfo: PointersInfo): Boolean {
+    private fun startDragImmediately(pointersDown: PointersInfo.PointersDown): Boolean {
         // Immediately start the drag if the user can't swipe in the other direction and the gesture
         // handler can intercept it.
-        return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersInfo)
+        return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersDown)
     }
 
     private fun canOppositeSwipe(): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 7d29a68..e3118d67 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.spring
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.runtime.Stable
@@ -34,8 +35,6 @@
 import com.android.compose.animation.scene.TransformationSpec
 import com.android.compose.animation.scene.TransformationSpecImpl
 import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.transition.link.LinkedTransition
-import com.android.compose.animation.scene.transition.link.StateLink
 import kotlinx.coroutines.launch
 
 /** The state associated to a [SceneTransitionLayout] at some specific point in time. */
@@ -280,8 +279,8 @@
          */
         private var interruptionDecay: Animatable<Float, AnimationVector1D>? = null
 
-        /** The map of active links that connects this transition to other transitions. */
-        internal val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()
+        /** Whether this transition was already started. */
+        private var wasStarted = false
 
         init {
             check(fromContent != toContent)
@@ -328,7 +327,7 @@
         }
 
         /** Run this transition and return once it is finished. */
-        abstract suspend fun run()
+        protected abstract suspend fun run()
 
         /**
          * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will
@@ -341,6 +340,13 @@
          */
         abstract fun freezeAndAnimateToCurrentState()
 
+        internal suspend fun runInternal() {
+            check(!wasStarted) { "A Transition can be started only once." }
+            wasStarted = true
+
+            run()
+        }
+
         internal fun updateOverscrollSpecs(
             fromSpec: OverscrollSpecImpl?,
             toSpec: OverscrollSpecImpl?,
@@ -376,7 +382,7 @@
                     val progressSpec =
                         spring(
                             stiffness = swipeSpec.stiffness,
-                            dampingRatio = swipeSpec.dampingRatio,
+                            dampingRatio = Spring.DampingRatioNoBouncy,
                             visibilityThreshold = ProgressVisibilityThreshold,
                         )
                     animatable.animateTo(0f, progressSpec)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
deleted file mode 100644
index 42ba9ba..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ /dev/null
@@ -1,59 +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.compose.animation.scene.transition.link
-
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.content.state.TransitionState
-
-/** A linked transition which is driven by a [originalTransition]. */
-internal class LinkedTransition(
-    private val originalTransition: TransitionState.Transition,
-    fromScene: SceneKey,
-    toScene: SceneKey,
-    override val key: TransitionKey? = null,
-) : TransitionState.Transition.ChangeScene(fromScene, toScene) {
-
-    override val currentScene: SceneKey
-        get() {
-            return when (originalTransition.currentScene) {
-                originalTransition.fromContent -> fromScene
-                originalTransition.toContent -> toScene
-                else -> error("Original currentScene is neither FromScene nor ToScene")
-            }
-        }
-
-    override val isInitiatedByUserInput: Boolean
-        get() = originalTransition.isInitiatedByUserInput
-
-    override val isUserInputOngoing: Boolean
-        get() = originalTransition.isUserInputOngoing
-
-    override val progress: Float
-        get() = originalTransition.progress
-
-    override val progressVelocity: Float
-        get() = originalTransition.progressVelocity
-
-    override suspend fun run() {
-        originalTransition.run()
-    }
-
-    override fun freezeAndAnimateToCurrentState() {
-        originalTransition.freezeAndAnimateToCurrentState()
-    }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
deleted file mode 100644
index 2aec509..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
+++ /dev/null
@@ -1,63 +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.compose.animation.scene.transition.link
-
-import com.android.compose.animation.scene.ContentKey
-import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayoutState
-import com.android.compose.animation.scene.TransitionKey
-import com.android.compose.animation.scene.content.state.TransitionState
-
-/** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */
-class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) {
-
-    internal val target = target as MutableSceneTransitionLayoutStateImpl
-
-    /**
-     * Links two transitions (source and target) together.
-     *
-     * `null` can be passed to indicate that any SceneKey should match. e.g. passing `null`, `null`,
-     * `null`, `SceneA` means that any transition at the source will trigger a transition in the
-     * target to `SceneA` from any current scene.
-     */
-    class TransitionLink(
-        val sourceFrom: ContentKey?,
-        val sourceTo: ContentKey?,
-        val targetFrom: SceneKey?,
-        val targetTo: SceneKey,
-        val targetTransitionKey: TransitionKey? = null,
-    ) {
-        init {
-            if (
-                (sourceFrom != null && sourceFrom == sourceTo) ||
-                    (targetFrom != null && targetFrom == targetTo)
-            )
-                error("From and To can't be the same")
-        }
-
-        internal fun isMatchingLink(transition: TransitionState.Transition): Boolean {
-            return (sourceFrom == null || sourceFrom == transition.fromContent) &&
-                (sourceTo == null || sourceTo == transition.toContent)
-        }
-
-        internal fun targetIsInValidState(targetCurrentContent: ContentKey): Boolean {
-            return (targetFrom == null || targetFrom == targetCurrentContent) &&
-                targetTo != targetCurrentContent
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 098673e..7e6f3a8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -52,17 +52,15 @@
 private const val SCREEN_SIZE = 100f
 private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
 
-private fun pointersInfo(
+private fun pointersDown(
     startedPosition: Offset = Offset.Zero,
     pointersDown: Int = 1,
-    isMouseWheel: Boolean = false,
     pointersDownByType: Map<PointerType, Int> = mapOf(PointerType.Touch to pointersDown),
-): PointersInfo {
-    return PointersInfo(
+): PointersInfo.PointersDown {
+    return PointersInfo.PointersDown(
         startedPosition = startedPosition,
-        pointersDown = pointersDown,
-        isMouseWheel = isMouseWheel,
-        pointersDownByType = pointersDownByType,
+        count = pointersDown,
+        countByType = pointersDownByType,
     )
 }
 
@@ -138,7 +136,7 @@
         val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
         val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
 
-        var pointerInfoOwner: () -> PointersInfo = { pointersInfo() }
+        var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
 
         fun nestedScrollConnection(
             nestedScrollBehavior: NestedScrollBehavior,
@@ -221,7 +219,7 @@
         }
 
         fun onDragStarted(
-            pointersInfo: PointersInfo = pointersInfo(),
+            pointersInfo: PointersInfo.PointersDown = pointersDown(),
             overSlop: Float,
             expectedConsumedOverSlop: Float = overSlop,
         ): DragController {
@@ -235,18 +233,20 @@
             )
         }
 
-        fun onDragStartedImmediately(pointersInfo: PointersInfo = pointersInfo()): DragController {
+        fun onDragStartedImmediately(
+            pointersInfo: PointersInfo.PointersDown = pointersDown()
+        ): DragController {
             return onDragStarted(draggableHandler, pointersInfo, overSlop = 0f)
         }
 
         fun onDragStarted(
             draggableHandler: DraggableHandler,
-            pointersInfo: PointersInfo = pointersInfo(),
+            pointersInfo: PointersInfo.PointersDown = pointersDown(),
             overSlop: Float = 0f,
             expectedConsumedOverSlop: Float = overSlop,
         ): DragController {
             val dragController =
-                draggableHandler.onDragStarted(pointersInfo = pointersInfo, overSlop = overSlop)
+                draggableHandler.onDragStarted(pointersDown = pointersInfo, overSlop = overSlop)
 
             // MultiPointerDraggable will always call onDelta with the initial overSlop right after
             dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop)
@@ -529,7 +529,7 @@
         val dragController =
             onDragStarted(
                 pointersInfo =
-                    pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)),
+                    pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)),
                 overSlop = up(fractionOfScreen = 0.2f),
             )
         assertTransition(
@@ -555,7 +555,7 @@
 
         // Start dragging from the bottom
         onDragStarted(
-            pointersInfo = pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)),
+            pointersInfo = pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)),
             overSlop = up(fractionOfScreen = 0.1f),
         )
         assertTransition(
@@ -1052,7 +1052,7 @@
         navigateToSceneC()
 
         // Swipe up from the middle to transition to scene B.
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
         assertTransition(
             currentScene = SceneC,
@@ -1084,7 +1084,7 @@
         // Start a new gesture from the bottom of the screen. Because swiping up from the bottom of
         // C leads to scene A (and not B), the previous transitions is *not* intercepted and we
         // instead animate from C to A.
-        val bottom = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2, SCREEN_SIZE))
+        val bottom = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2, SCREEN_SIZE))
         assertThat(draggableHandler.shouldImmediatelyIntercept(bottom)).isFalse()
         onDragStarted(pointersInfo = bottom, overSlop = up(0.1f))
 
@@ -1103,7 +1103,7 @@
         navigateToSceneC()
 
         // Swipe up from the middle to transition to scene B.
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
         assertTransition(fromScene = SceneC, toScene = SceneB, isUserInputOngoing = true)
 
@@ -1120,15 +1120,15 @@
 
     @Test
     fun interruptedTransitionCanNotBeImmediatelyIntercepted() = runGestureTest {
-        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse()
+        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isFalse()
         onDragStarted(overSlop = up(0.1f))
-        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue()
+        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isTrue()
 
         layoutState.startTransitionImmediately(
             animationScope = testScope.backgroundScope,
             transition(SceneA, SceneB),
         )
-        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse()
+        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isFalse()
     }
 
     @Test
@@ -1160,7 +1160,7 @@
         assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
 
         // Intercept the transition and swipe down back to scene A.
-        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue()
+        assertThat(draggableHandler.shouldImmediatelyIntercept(pointersDown = null)).isTrue()
         val dragController2 = onDragStartedImmediately()
 
         // Block the transition when the user release their finger.
@@ -1204,7 +1204,7 @@
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
 
         // Drag from the **top** of the screen
-        pointerInfoOwner = { pointersInfo() }
+        pointerInfoOwner = { pointersDown() }
         assertIdle(currentScene = SceneC)
 
         nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1221,7 +1221,7 @@
         advanceUntilIdle()
 
         // Drag from the **bottom** of the screen
-        pointerInfoOwner = { pointersInfo(startedPosition = Offset(0f, SCREEN_SIZE)) }
+        pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
         assertIdle(currentScene = SceneC)
 
         nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1241,7 +1241,7 @@
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
 
         // Use mouse wheel
-        pointerInfoOwner = { pointersInfo(isMouseWheel = true) }
+        pointerInfoOwner = { PointersInfo.MouseWheel }
         assertIdle(currentScene = SceneC)
 
         nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
@@ -1251,7 +1251,7 @@
     @Test
     fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
         // Swipe up from the middle to transition to scene B.
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.1f))
         assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true)
 
@@ -1265,7 +1265,7 @@
         layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }
 
         // Swipe up to scene B at progress = 200%.
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         val dragController =
             onDragStarted(
                 pointersInfo = middle,
@@ -1296,7 +1296,7 @@
         layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }
 
         // Swipe up to scene B at progress = 200%.
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.99f))
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.99f)
 
@@ -1342,7 +1342,7 @@
             overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
 
         val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.5f))
         val transition = assertThat(transitionState).isSceneTransition()
@@ -1374,7 +1374,7 @@
             overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
 
         val dragController = onDragStarted(pointersInfo = middle, overSlop = down(0.5f))
         val transition = assertThat(transitionState).isSceneTransition()
@@ -1405,7 +1405,7 @@
             overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
 
         val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1.5f))
         val transition = assertThat(transitionState).isSceneTransition()
@@ -1437,7 +1437,7 @@
             overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
         }
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
 
         val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1.5f))
         val transition = assertThat(transitionState).isSceneTransition()
@@ -1471,7 +1471,7 @@
 
         mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB))
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f))
         val transition = assertThat(transitionState).isSceneTransition()
         assertThat(transition).hasFromScene(SceneA)
@@ -1504,7 +1504,7 @@
 
         mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC))
 
-        val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
+        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
         val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f))
         val transition = assertThat(transitionState).isSceneTransition()
         assertThat(transition).hasFromScene(SceneA)
@@ -1676,4 +1676,33 @@
         assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
         assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
     }
+
+    @Test
+    fun replaceOverlayNestedScroll() = runGestureTest {
+        layoutState.showOverlay(OverlayA, animationScope = testScope)
+        advanceUntilIdle()
+
+        // Initial state.
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
+
+        // Swipe down to replace overlay A by overlay B.
+
+        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
+        nestedScroll.scroll(downOffset(0.1f))
+        val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
+        assertThat(transition).hasCurrentScene(SceneA)
+        assertThat(transition).hasFromOverlay(OverlayA)
+        assertThat(transition).hasToOverlay(OverlayB)
+        assertThat(transition).hasCurrentOverlays(OverlayA)
+        assertThat(transition).hasProgress(0.1f)
+
+        nestedScroll.preFling(Velocity(0f, velocityThreshold))
+        advanceUntilIdle()
+        // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+        assertThat(layoutState.transitionState).isIdle()
+        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+        assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index a2b263b..2b70908 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -26,10 +26,8 @@
 import com.android.compose.animation.scene.TestScenes.SceneA
 import com.android.compose.animation.scene.TestScenes.SceneB
 import com.android.compose.animation.scene.TestScenes.SceneC
-import com.android.compose.animation.scene.TestScenes.SceneD
 import com.android.compose.animation.scene.content.state.TransitionState
 import com.android.compose.animation.scene.subjects.assertThat
-import com.android.compose.animation.scene.transition.link.StateLink
 import com.android.compose.animation.scene.transition.seekToScene
 import com.android.compose.test.MonotonicClockTestScope
 import com.android.compose.test.TestSceneTransition
@@ -42,6 +40,7 @@
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.consumeAsFlow
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertThrows
 import org.junit.Rule
@@ -132,147 +131,6 @@
         assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB))
     }
 
-    private fun setupLinkedStates(
-        parentInitialScene: SceneKey = SceneC,
-        childInitialScene: SceneKey = SceneA,
-        sourceFrom: SceneKey? = SceneA,
-        sourceTo: SceneKey? = SceneB,
-        targetFrom: SceneKey? = SceneC,
-        targetTo: SceneKey = SceneD,
-    ): Pair<MutableSceneTransitionLayoutStateImpl, MutableSceneTransitionLayoutStateImpl> {
-        val parentState = MutableSceneTransitionLayoutState(parentInitialScene)
-        val link =
-            listOf(
-                StateLink(
-                    parentState,
-                    listOf(StateLink.TransitionLink(sourceFrom, sourceTo, targetFrom, targetTo)),
-                )
-            )
-        val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link)
-        return Pair(
-            parentState as MutableSceneTransitionLayoutStateImpl,
-            childState as MutableSceneTransitionLayoutStateImpl,
-        )
-    }
-
-    @Test
-    fun linkedTransition_startsLinkAndFinishesLinkInToState() = runTest {
-        val (parentState, childState) = setupLinkedStates()
-
-        val childTransition = transition(SceneA, SceneB)
-
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
-        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
-    }
-
-    @Test
-    fun linkedTransition_transitiveLink() = runTest {
-        val parentParentState =
-            MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl
-        val parentLink =
-            listOf(
-                StateLink(
-                    parentParentState,
-                    listOf(StateLink.TransitionLink(SceneC, SceneD, SceneB, SceneC)),
-                )
-            )
-        val parentState =
-            MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink)
-                as MutableSceneTransitionLayoutStateImpl
-        val link =
-            listOf(
-                StateLink(
-                    parentState,
-                    listOf(StateLink.TransitionLink(SceneA, SceneB, SceneC, SceneD)),
-                )
-            )
-        val childState =
-            MutableSceneTransitionLayoutState(SceneA, stateLinks = link)
-                as MutableSceneTransitionLayoutStateImpl
-
-        val childTransition = transition(SceneA, SceneB)
-
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
-        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-        assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue()
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
-        assertThat(parentParentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-    }
-
-    @Test
-    fun linkedTransition_linkProgressIsEqual() = runTest {
-        val (parentState, childState) = setupLinkedStates()
-
-        var progress = 0f
-        val childTransition = transition(SceneA, SceneB, progress = { progress })
-
-        childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(parentState.currentTransition?.progress).isEqualTo(0f)
-
-        progress = .5f
-        assertThat(parentState.currentTransition?.progress).isEqualTo(.5f)
-    }
-
-    @Test
-    fun linkedTransition_reverseTransitionIsNotLinked() = runTest {
-        val (parentState, childState) = setupLinkedStates()
-
-        val childTransition = transition(SceneB, SceneA, current = { SceneB })
-
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue()
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-    }
-
-    @Test
-    fun linkedTransition_startsLinkAndFinishesLinkInFromState() = runTest {
-        val (parentState, childState) = setupLinkedStates()
-
-        val childTransition = transition(SceneA, SceneB, current = { SceneA })
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-    }
-
-    @Test
-    fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest {
-        val (parentState, childState) = setupLinkedStates()
-
-        val childTransition = transition(SceneA, SceneB)
-        val parentTransition = transition(SceneC, SceneA)
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        parentState.startTransitionImmediately(animationScope = backgroundScope, parentTransition)
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
-        assertThat(parentState.transitionState).isEqualTo(parentTransition)
-    }
-
     @Test
     fun setTargetScene_withTransitionKey() = runMonotonicClockTest {
         val transitionkey = TransitionKey(debugName = "foo")
@@ -393,51 +251,6 @@
         assertThat(state.isTransitioning()).isTrue()
     }
 
-    @Test
-    fun linkedTransition_fuzzyLinksAreMatchedAndStarted() = runTest {
-        val (parentState, childState) = setupLinkedStates(SceneC, SceneA, null, null, null, SceneD)
-        val childTransition = transition(SceneA, SceneB)
-
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
-        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD))
-    }
-
-    @Test
-    fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() = runTest {
-        val (parentState, childState) =
-            setupLinkedStates(SceneC, SceneA, SceneA, null, null, SceneD)
-
-        val childTransition = transition(SceneA, SceneB, current = { SceneA })
-
-        val job =
-            childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
-        assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue()
-
-        childTransition.finish()
-        job.join()
-        assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA))
-        assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC))
-    }
-
-    @Test
-    fun linkedTransition_fuzzyLinksAreNotMatched() = runTest {
-        val (parentState, childState) =
-            setupLinkedStates(SceneC, SceneA, SceneB, null, SceneC, SceneD)
-        val childTransition = transition(SceneA, SceneB)
-
-        childState.startTransitionImmediately(animationScope = backgroundScope, childTransition)
-        assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue()
-        assertThat(parentState.isTransitioning(SceneC, SceneD)).isFalse()
-    }
-
     private fun MonotonicClockTestScope.startOverscrollableTransistionFromAtoB(
         progress: () -> Float,
         sceneTransitions: SceneTransitions,
@@ -777,4 +590,15 @@
         assertThat(transition.progressTo(SceneA)).isEqualTo(1f - 0.2f)
         assertThrows(IllegalArgumentException::class.java) { transition.progressTo(SceneC) }
     }
+
+    @Test
+    fun transitionCanBeStartedOnlyOnce() = runTest {
+        val state = MutableSceneTransitionLayoutState(SceneA)
+        val transition = transition(from = SceneA, to = SceneB)
+
+        state.startTransitionImmediately(backgroundScope, transition)
+        assertThrows(IllegalStateException::class.java) {
+            runBlocking { state.startTransition(transition) }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 3b2ee98..97a96a4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -574,7 +574,7 @@
         rule.setContent {
             touchSlop = LocalViewConfiguration.current.touchSlop
             SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) {
-                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+                scene(SceneA, userActions = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneB)) {
                     Box(
                         Modifier.fillMaxSize()
                             // A scrollable that does not consume the scroll gesture
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
deleted file mode 100644
index dd58ea7..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2021 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.keyguard;
-
-import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.hardware.display.DisplayManagerGlobal;
-import android.testing.TestableLooper;
-import android.view.Display;
-import android.view.DisplayInfo;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.navigationbar.NavigationBarController;
-import com.android.systemui.settings.FakeDisplayTracker;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.concurrent.Executor;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper
-public class KeyguardDisplayManagerTest extends SysuiTestCase {
-
-    @Mock
-    private NavigationBarController mNavigationBarController;
-    @Mock
-    private ConnectedDisplayKeyguardPresentation.Factory
-            mConnectedDisplayKeyguardPresentationFactory;
-    @Mock
-    private ConnectedDisplayKeyguardPresentation mConnectedDisplayKeyguardPresentation;
-    @Mock
-    private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper;
-    @Mock
-    private KeyguardStateController mKeyguardStateController;
-
-    private Executor mMainExecutor = Runnable::run;
-    private Executor mBackgroundExecutor = Runnable::run;
-    private KeyguardDisplayManager mManager;
-    private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
-    // The default and secondary displays are both in the default group
-    private Display mDefaultDisplay;
-    private Display mSecondaryDisplay;
-
-    // This display is in a different group from the default and secondary displays.
-    private Display mAlwaysUnlockedDisplay;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController,
-                mDisplayTracker, mMainExecutor, mBackgroundExecutor, mDeviceStateHelper,
-                mKeyguardStateController, mConnectedDisplayKeyguardPresentationFactory));
-        doReturn(mConnectedDisplayKeyguardPresentation).when(
-                mConnectedDisplayKeyguardPresentationFactory).create(any());
-        doReturn(mConnectedDisplayKeyguardPresentation).when(mManager)
-                .createPresentation(any());
-        mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
-                new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
-        mSecondaryDisplay = new Display(DisplayManagerGlobal.getInstance(),
-                Display.DEFAULT_DISPLAY + 1,
-                new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
-
-        DisplayInfo alwaysUnlockedDisplayInfo = new DisplayInfo();
-        alwaysUnlockedDisplayInfo.displayId = Display.DEFAULT_DISPLAY + 2;
-        alwaysUnlockedDisplayInfo.flags = Display.FLAG_ALWAYS_UNLOCKED;
-        mAlwaysUnlockedDisplay = new Display(DisplayManagerGlobal.getInstance(),
-                Display.DEFAULT_DISPLAY,
-                alwaysUnlockedDisplayInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
-    }
-
-    @Test
-    public void testShow_defaultDisplayOnly() {
-        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay});
-        mManager.show();
-        verify(mManager, never()).createPresentation(any());
-    }
-
-    @Test
-    public void testShow_includeSecondaryDisplay() {
-        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
-        mManager.show();
-        verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
-    }
-
-    @Test
-    public void testShow_includeAlwaysUnlockedDisplay() {
-        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mAlwaysUnlockedDisplay});
-
-        mManager.show();
-        verify(mManager, never()).createPresentation(any());
-    }
-
-    @Test
-    public void testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
-        mDisplayTracker.setAllDisplays(
-                new Display[]{mDefaultDisplay, mSecondaryDisplay, mAlwaysUnlockedDisplay});
-
-        mManager.show();
-        verify(mManager, times(1)).createPresentation(eq(mSecondaryDisplay));
-    }
-
-    @Test
-    public void testShow_concurrentDisplayActive_occluded() {
-        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
-
-        when(mDeviceStateHelper.isConcurrentDisplayActive(mSecondaryDisplay)).thenReturn(true);
-        when(mKeyguardStateController.isOccluded()).thenReturn(true);
-        verify(mManager, never()).createPresentation(eq(mSecondaryDisplay));
-    }
-
-    @Test
-    public void testShow_presentationCreated() {
-        when(mManager.createPresentation(any())).thenCallRealMethod();
-        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
-
-        mManager.show();
-
-        verify(mConnectedDisplayKeyguardPresentationFactory).create(eq(mSecondaryDisplay));
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
new file mode 100644
index 0000000..57a6797
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.keyguard
+
+import android.hardware.display.DisplayManagerGlobal
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper.RunWithLooper
+import android.view.Display
+import android.view.DisplayAdjustments
+import android.view.DisplayInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardDisplayManager.DeviceStateHelper
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.navigationbar.NavigationBarController
+import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.shade.data.repository.FakeShadePositionRepository
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+class KeyguardDisplayManagerTest : SysuiTestCase() {
+    @Mock private val navigationBarController = mock(NavigationBarController::class.java)
+    @Mock
+    private val presentationFactory = mock(ConnectedDisplayKeyguardPresentation.Factory::class.java)
+    @Mock
+    private val connectedDisplayKeyguardPresentation =
+        mock(ConnectedDisplayKeyguardPresentation::class.java)
+    @Mock private val deviceStateHelper = mock(DeviceStateHelper::class.java)
+    @Mock private val keyguardStateController = mock(KeyguardStateController::class.java)
+    private val shadePositionRepository = FakeShadePositionRepository()
+
+    private val mainExecutor = Executor { it.run() }
+    private val backgroundExecutor = Executor { it.run() }
+    private lateinit var manager: KeyguardDisplayManager
+    private val displayTracker = FakeDisplayTracker(mContext)
+    // The default and secondary displays are both in the default group
+    private lateinit var defaultDisplay: Display
+    private lateinit var secondaryDisplay: Display
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    // This display is in a different group from the default and secondary displays.
+    private lateinit var alwaysUnlockedDisplay: Display
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        manager =
+            KeyguardDisplayManager(
+                mContext,
+                { navigationBarController },
+                displayTracker,
+                mainExecutor,
+                backgroundExecutor,
+                deviceStateHelper,
+                keyguardStateController,
+                presentationFactory,
+                { shadePositionRepository },
+                testScope.backgroundScope,
+            )
+        whenever(presentationFactory.create(any())).doReturn(connectedDisplayKeyguardPresentation)
+
+        defaultDisplay =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                Display.DEFAULT_DISPLAY,
+                DisplayInfo(),
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+            )
+        secondaryDisplay =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                Display.DEFAULT_DISPLAY + 1,
+                DisplayInfo(),
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+            )
+
+        val alwaysUnlockedDisplayInfo = DisplayInfo()
+        alwaysUnlockedDisplayInfo.displayId = Display.DEFAULT_DISPLAY + 2
+        alwaysUnlockedDisplayInfo.flags = Display.FLAG_ALWAYS_UNLOCKED
+        alwaysUnlockedDisplay =
+            Display(
+                DisplayManagerGlobal.getInstance(),
+                Display.DEFAULT_DISPLAY,
+                alwaysUnlockedDisplayInfo,
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
+            )
+    }
+
+    @Test
+    fun testShow_defaultDisplayOnly() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay)
+        manager.show()
+        verify(presentationFactory, never()).create(any())
+    }
+
+    @Test
+    fun testShow_includeSecondaryDisplay() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+        manager.show()
+        verify(presentationFactory).create(eq(secondaryDisplay))
+    }
+
+    @Test
+    fun testShow_includeAlwaysUnlockedDisplay() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, alwaysUnlockedDisplay)
+
+        manager.show()
+        verify(presentationFactory, never()).create(any())
+    }
+
+    @Test
+    fun testShow_includeSecondaryAndAlwaysUnlockedDisplays() {
+        displayTracker.allDisplays =
+            arrayOf(defaultDisplay, secondaryDisplay, alwaysUnlockedDisplay)
+
+        manager.show()
+        verify(presentationFactory).create(eq(secondaryDisplay))
+    }
+
+    @Test
+    fun testShow_concurrentDisplayActive_occluded() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+
+        whenever(deviceStateHelper.isConcurrentDisplayActive(secondaryDisplay)).thenReturn(true)
+        whenever(keyguardStateController.isOccluded).thenReturn(true)
+        verify(presentationFactory, never()).create(eq(secondaryDisplay))
+    }
+
+    @Test
+    fun testShow_presentationCreated() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+
+        manager.show()
+
+        verify(presentationFactory).create(eq(secondaryDisplay))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun show_shadeMovesDisplay_newPresentationCreated() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+        // Shade in the default display, we expect the presentation to be in the secondary only
+        shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+
+        manager.show()
+
+        verify(presentationFactory).create(eq(secondaryDisplay))
+        verify(presentationFactory, never()).create(eq(defaultDisplay))
+        reset(presentationFactory)
+        whenever(presentationFactory.create(any())).thenReturn(connectedDisplayKeyguardPresentation)
+
+        // Let's move it to the secondary display. We expect it will be added in the default
+        // one.
+        shadePositionRepository.setDisplayId(secondaryDisplay.displayId)
+        testScope.advanceUntilIdle()
+
+        verify(presentationFactory).create(eq(defaultDisplay))
+        reset(presentationFactory)
+        whenever(presentationFactory.create(any())).thenReturn(connectedDisplayKeyguardPresentation)
+
+        // Let's move it back! it should be re-created (it means it was removed before)
+        shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+        testScope.advanceUntilIdle()
+
+        verify(presentationFactory).create(eq(secondaryDisplay))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun show_shadeInSecondaryDisplay_defaultOneHasPresentation() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+        shadePositionRepository.setDisplayId(secondaryDisplay.displayId)
+
+        manager.show()
+
+        verify(presentationFactory).create(eq(defaultDisplay))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND)
+    fun show_shadeInDefaultDisplay_secondaryOneHasPresentation() {
+        displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay)
+        shadePositionRepository.setDisplayId(defaultDisplay.displayId)
+
+        manager.show()
+
+        verify(presentationFactory).create(eq(secondaryDisplay))
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
new file mode 100644
index 0000000..c646d8f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
@@ -0,0 +1,291 @@
+/*
+ * 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 android.content.Context
+import android.content.Context.INPUT_SERVICE
+import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.createKeyTrigger
+import android.hardware.input.KeyGestureEvent
+import android.hardware.input.fakeInputManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.KeyEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
+import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository
+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.AppCategories
+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.ShortcutKey
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() {
+
+    private val mockUserContext: Context = mock()
+    private val kosmos =
+        testKosmos().also {
+            it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+        }
+
+    private val fakeInputManager = kosmos.fakeInputManager
+    private val testScope = kosmos.testScope
+    private val helper = kosmos.shortcutHelperTestHelper
+    private val repo = kosmos.customShortcutCategoriesRepository
+
+    @Before
+    fun setup() {
+        whenever(mockUserContext.getSystemService(INPUT_SERVICE))
+            .thenReturn(fakeInputManager.inputManager)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    fun categories_emitsCorrectlyConvertedShortcutCategories() {
+        testScope.runTest {
+            whenever(
+                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+                )
+                .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)
+
+            helper.toggle(deviceId = 123)
+            val categories by collectLastValue(repo.categories)
+
+            assertThat(categories)
+                .containsExactlyElementsIn(expectedShortcutCategoriesWithSimpleShortcutCombination)
+        }
+    }
+
+    @Test
+    @DisableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    fun categories_emitsEmptyListWhenFlagIsDisabled() {
+        testScope.runTest {
+            whenever(
+                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+                )
+                .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations)
+
+            helper.toggle(deviceId = 123)
+            val categories by collectLastValue(repo.categories)
+
+            assertThat(categories).isEmpty()
+        }
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+    fun categories_ignoresUnknownKeyGestureTypes() {
+        testScope.runTest {
+            whenever(
+                    fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
+                )
+                .thenReturn(customizableInputGestureWithUnknownKeyGestureType)
+
+            helper.toggle(deviceId = 123)
+            val categories by collectLastValue(repo.categories)
+
+            assertThat(categories).isEmpty()
+        }
+    }
+
+    private fun simpleInputGestureData(
+        keyCode: Int = KeyEvent.KEYCODE_A,
+        modifiers: Int = KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+        keyGestureType: Int,
+    ): InputGestureData {
+        val builder = InputGestureData.Builder()
+        builder.setKeyGestureType(keyGestureType)
+        builder.setTrigger(createKeyTrigger(keyCode, modifiers))
+        return builder.build()
+    }
+
+    private fun simpleShortcutCategory(
+        category: ShortcutCategoryType,
+        subcategoryLabel: String,
+        shortcutLabel: String,
+    ): ShortcutCategory {
+        return ShortcutCategory(
+            type = category,
+            subCategories =
+                listOf(
+                    ShortcutSubCategory(
+                        label = subcategoryLabel,
+                        shortcuts = listOf(simpleShortcut(shortcutLabel)),
+                    )
+                ),
+        )
+    }
+
+    private fun simpleShortcut(label: String) =
+        Shortcut(
+            label = label,
+            commands =
+                listOf(
+                    ShortcutCommand(
+                        isCustom = true,
+                        keys =
+                            listOf(
+                                ShortcutKey.Text("Ctrl"),
+                                ShortcutKey.Text("Alt"),
+                                ShortcutKey.Text("A"),
+                            ),
+                    )
+                ),
+        )
+
+    private val customizableInputGestureWithUnknownKeyGestureType =
+        // These key gesture events are currently not supported by shortcut helper customizer
+        listOf(
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY),
+        )
+
+    private val expectedShortcutCategoriesWithSimpleShortcutCombination =
+        listOf(
+            simpleShortcutCategory(System, "System apps", "Open assistant"),
+            simpleShortcutCategory(System, "System controls", "Go to home screen"),
+            simpleShortcutCategory(System, "System apps", "Open settings"),
+            simpleShortcutCategory(System, "System controls", "Lock screen"),
+            simpleShortcutCategory(System, "System controls", "View notifications"),
+            simpleShortcutCategory(System, "System apps", "Take a note"),
+            simpleShortcutCategory(System, "System controls", "Take screenshot"),
+            simpleShortcutCategory(System, "System controls", "Go back"),
+            simpleShortcutCategory(
+                MultiTasking,
+                "Split screen",
+                "Switch from split screen to full screen",
+            ),
+            simpleShortcutCategory(
+                MultiTasking,
+                "Split screen",
+                "Use split screen with current app on the left",
+            ),
+            simpleShortcutCategory(
+                MultiTasking,
+                "Split screen",
+                "Switch to app on left or above while using split screen",
+            ),
+            simpleShortcutCategory(
+                MultiTasking,
+                "Split screen",
+                "Use split screen with current app on the right",
+            ),
+            simpleShortcutCategory(
+                MultiTasking,
+                "Split screen",
+                "Switch to app on right or below while using split screen",
+            ),
+            simpleShortcutCategory(System, "System controls", "Show shortcuts"),
+            simpleShortcutCategory(System, "System controls", "View recent apps"),
+            simpleShortcutCategory(AppCategories, "Applications", "Calculator"),
+            simpleShortcutCategory(AppCategories, "Applications", "Calendar"),
+            simpleShortcutCategory(AppCategories, "Applications", "Chrome"),
+            simpleShortcutCategory(AppCategories, "Applications", "Contacts"),
+            simpleShortcutCategory(AppCategories, "Applications", "Gmail"),
+            simpleShortcutCategory(AppCategories, "Applications", "Maps"),
+            simpleShortcutCategory(AppCategories, "Applications", "Messages"),
+            simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"),
+        )
+
+    private val customizableInputGesturesWithSimpleShortcutCombinations =
+        listOf(
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
+            ),
+            simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING
+            ),
+            simpleInputGestureData(
+                keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+            ),
+        )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
similarity index 98%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
index 620b8b6..f90ab1f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepositoryTest.kt
@@ -40,6 +40,7 @@
 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.defaultShortcutCategoriesRepository
 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
@@ -47,7 +48,6 @@
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
 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
@@ -71,7 +71,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() {
+class DefaultShortcutCategoriesRepositoryTest : SysuiTestCase() {
 
     private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
     private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
@@ -87,7 +87,7 @@
             it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource()
         }
 
-    private val repo = kosmos.shortcutHelperCategoriesRepository
+    private val repo = kosmos.defaultShortcutCategoriesRepository
     private val helper = kosmos.shortcutHelperTestHelper
     private val testScope = kosmos.testScope
     private val fakeInputManager = kosmos.fakeInputManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
index a862fdf..778e822 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
@@ -19,6 +19,8 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.view.Display
 import android.view.DisplayCutout
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,6 +31,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.commandline.CommandRegistry
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
@@ -1051,7 +1054,8 @@
     }
 
     @Test
-    fun onMaxBoundsChanged_beforeStart_listenerNotNotified() {
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onMaxBoundsChanged_beforeStart_flagEnabled_listenerNotNotified() {
         // Start out with an existing configuration with bounds
         configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
         configurationController.onConfigurationChanged(configuration)
@@ -1083,7 +1087,41 @@
     }
 
     @Test
-    fun onDensityOrFontScaleChanged_beforeStart_listenerNotNotified() {
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onMaxBoundsChanged_beforeStart_flagDisabled_listenerNotNotified() {
+        // Start out with an existing configuration with bounds
+        configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100)
+        configurationController.onConfigurationChanged(configuration)
+        val provider =
+            StatusBarContentInsetsProviderImpl(
+                contextMock,
+                configurationController,
+                mock<DumpManager>(),
+                mock<CommandRegistry>(),
+                mock<SysUICutoutProvider>(),
+            )
+        val listener =
+            object : StatusBarContentInsetsChangedListener {
+                var triggered = false
+
+                override fun onStatusBarContentInsetsChanged() {
+                    triggered = true
+                }
+            }
+        provider.addCallback(listener)
+
+        // WHEN the config is updated with new bounds
+        // but provider is not started
+        configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789)
+        configurationController.onConfigurationChanged(configuration)
+
+        // THEN the listener is notified
+        assertThat(listener.triggered).isTrue()
+    }
+
+    @Test
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onDensityOrFontScaleChanged_beforeStart_flagEnabled_listenerNotNotified() {
         configuration.densityDpi = 12
         val provider =
             StatusBarContentInsetsProviderImpl(
@@ -1112,6 +1150,36 @@
     }
 
     @Test
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onDensityOrFontScaleChanged_beforeStart_flagDisabled_listenerNotified() {
+        configuration.densityDpi = 12
+        val provider =
+            StatusBarContentInsetsProviderImpl(
+                contextMock,
+                configurationController,
+                mock<DumpManager>(),
+                mock<CommandRegistry>(),
+                mock<SysUICutoutProvider>(),
+            )
+        val listener =
+            object : StatusBarContentInsetsChangedListener {
+                var triggered = false
+
+                override fun onStatusBarContentInsetsChanged() {
+                    triggered = true
+                }
+            }
+        provider.addCallback(listener)
+
+        // WHEN the config is updated, but the provider is not started
+        configuration.densityDpi = 20
+        configurationController.onConfigurationChanged(configuration)
+
+        // THEN the listener is notified
+        assertThat(listener.triggered).isTrue()
+    }
+
+    @Test
     fun onDensityOrFontScaleChanged_afterStart_listenerNotified() {
         configuration.densityDpi = 12
         val provider =
@@ -1169,7 +1237,8 @@
     }
 
     @Test
-    fun onThemeChanged_beforeStart_listenerNotNotified() {
+    @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onThemeChanged_beforeStart_flagEnabled_listenerNotNotified() {
         val provider =
             StatusBarContentInsetsProviderImpl(
                 contextMock,
@@ -1193,6 +1262,32 @@
         assertThat(listener.triggered).isFalse()
     }
 
+    @Test
+    @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+    fun onThemeChanged_beforeStart_flagDisabled_listenerNotified() {
+        val provider =
+            StatusBarContentInsetsProviderImpl(
+                contextMock,
+                configurationController,
+                mock<DumpManager>(),
+                mock<CommandRegistry>(),
+                mock<SysUICutoutProvider>(),
+            )
+        val listener =
+            object : StatusBarContentInsetsChangedListener {
+                var triggered = false
+
+                override fun onStatusBarContentInsetsChanged() {
+                    triggered = true
+                }
+            }
+        provider.addCallback(listener)
+
+        configurationController.notifyThemeChanged()
+
+        assertThat(listener.triggered).isTrue()
+    }
+
     private fun assertRects(
         expected: Rect,
         actual: Rect,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
index cdc7aa2..9888574 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewBinder.kt
@@ -32,6 +32,8 @@
     override fun bind(
         view: View,
         viewModel: HomeStatusBarViewModel,
+        systemEventChipAnimateIn: ((View) -> Unit)?,
+        systemEventChipAnimateOut: ((View) -> Unit)?,
         listener: StatusBarVisibilityChangeListener,
     ) {
         this.listener = listener
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
index 02c1540..eef5753 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt
@@ -19,6 +19,7 @@
 import android.view.View
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -53,11 +54,14 @@
             )
         )
 
-    override val isSystemInfoVisible =
+    override val systemInfoCombinedVis =
         MutableStateFlow(
-            HomeStatusBarViewModel.VisibilityModel(
-                visibility = View.GONE,
-                shouldAnimateChange = false,
+            HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+                HomeStatusBarViewModel.VisibilityModel(
+                    visibility = View.GONE,
+                    shouldAnimateChange = false,
+                ),
+                Idle,
             )
         )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index b3a73d8..c4d2569 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -60,11 +60,16 @@
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.emptyFlow
@@ -90,6 +95,7 @@
     private val activeNotificationListRepository = kosmos.activeNotificationListRepository
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val disableFlagsRepository = kosmos.fakeDisableFlagsRepository
+    private val systemStatusEventAnimationRepository = kosmos.systemStatusEventAnimationRepository
 
     private lateinit var underTest: HomeStatusBarViewModel
 
@@ -546,25 +552,50 @@
     @Test
     fun isSystemInfoVisible_allowedByDisableFlags_visible() =
         testScope.runTest {
-            val latest by collectLastValue(underTest.isSystemInfoVisible)
+            val latest by collectLastValue(underTest.systemInfoCombinedVis)
             transitionKeyguardToGone()
 
             disableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE)
 
-            assertThat(latest!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(latest!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
     fun isSystemInfoVisible_notAllowedByDisableFlags_gone() =
         testScope.runTest {
-            val latest by collectLastValue(underTest.isSystemInfoVisible)
+            val latest by collectLastValue(underTest.systemInfoCombinedVis)
             transitionKeyguardToGone()
 
             disableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(DISABLE_SYSTEM_INFO, DISABLE2_NONE)
 
-            assertThat(latest!!.visibility).isEqualTo(View.GONE)
+            assertThat(latest!!.baseVisibility.visibility).isEqualTo(View.GONE)
+        }
+
+    @Test
+    fun systemInfoCombineVis_animationsPassThrough() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.systemInfoCombinedVis)
+            transitionKeyguardToGone()
+
+            assertThat(latest!!.baseVisibility)
+                .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+            assertThat(latest!!.animationState).isEqualTo(Idle)
+
+            // WHEN the animation state changes, but the visibility state doesn't change
+            systemStatusEventAnimationRepository.animationState.value = AnimatingIn
+
+            // THEN the visibility is the same
+            assertThat(latest!!.baseVisibility)
+                .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+            // THEN the animation state updates
+            assertThat(latest!!.animationState).isEqualTo(AnimatingIn)
+
+            systemStatusEventAnimationRepository.animationState.value = AnimatingOut
+            assertThat(latest!!.baseVisibility)
+                .isEqualTo(VisibilityModel(visibility = View.VISIBLE, shouldAnimateChange = false))
+            assertThat(latest!!.animationState).isEqualTo(AnimatingOut)
         }
 
     @Test
@@ -573,7 +604,7 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.GONE,
@@ -583,7 +614,7 @@
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -592,13 +623,13 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -607,7 +638,7 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
@@ -617,7 +648,7 @@
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -626,13 +657,13 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Bouncer)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -641,7 +672,7 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
@@ -651,7 +682,7 @@
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
@@ -660,14 +691,14 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, taskInfo = null)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
@@ -676,13 +707,13 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             transitionKeyguardToGone()
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
@@ -691,14 +722,14 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
             transitionKeyguardToGone()
 
             kosmos.shadeTestUtil.setShadeExpansion(0f)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
@@ -707,13 +738,13 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Gone)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.VISIBLE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.VISIBLE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.VISIBLE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.VISIBLE)
         }
 
     @Test
@@ -722,14 +753,14 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
             transitionKeyguardToGone()
 
             kosmos.shadeTestUtil.setShadeExpansion(1f)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -738,14 +769,14 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
             transitionKeyguardToGone()
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Shade)
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -754,7 +785,7 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             // Secure camera is an occluding activity
             keyguardTransitionRepository.sendTransitionSteps(
@@ -766,7 +797,7 @@
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     @Test
@@ -775,7 +806,7 @@
         testScope.runTest {
             val clockVisible by collectLastValue(underTest.isClockVisible)
             val notifIconsVisible by collectLastValue(underTest.isNotificationIconContainerVisible)
-            val systemInfoVisible by collectLastValue(underTest.isSystemInfoVisible)
+            val systemInfoVisible by collectLastValue(underTest.systemInfoCombinedVis)
 
             kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen)
             // Secure camera is an occluding activity
@@ -784,7 +815,7 @@
 
             assertThat(clockVisible!!.visibility).isEqualTo(View.GONE)
             assertThat(notifIconsVisible!!.visibility).isEqualTo(View.GONE)
-            assertThat(systemInfoVisible!!.visibility).isEqualTo(View.GONE)
+            assertThat(systemInfoVisible!!.baseVisibility.visibility).isEqualTo(View.GONE)
         }
 
     private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) =
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index e90f055f..f187ce6 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,50 +13,58 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/volume_dialog_container"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_gravity="right"
-    android:divider="@drawable/volume_dialog_floating_sliders_spacer"
-    android:orientation="horizontal"
-    android:showDividers="middle|end|beginning"
-    android:theme="@style/volume_dialog_theme">
+    android:id="@+id/volume_dialog_root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
 
     <LinearLayout
-        android:id="@+id/volume_dialog_floating_sliders_container"
+        android:id="@+id/volume_dialog_container"
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
-        android:gravity="bottom"
-        android:orientation="horizontal"
-        android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
-        android:showDividers="middle" />
-
-    <LinearLayout
-        android:id="@+id/volume_dialog"
-        android:layout_width="@dimen/volume_dialog_width"
         android:layout_height="wrap_content"
-        android:background="@drawable/volume_dialog_background"
-        android:divider="@drawable/volume_dialog_spacer"
-        android:paddingVertical="@dimen/volume_dialog_vertical_padding"
-        android:gravity="center_horizontal"
-        android:orientation="vertical"
-        android:showDividers="middle">
+        android:layout_gravity="center_vertical|end"
+        android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+        android:orientation="horizontal"
+        android:showDividers="middle|end|beginning">
 
-        <include layout="@layout/volume_ringer_drawer" />
+        <LinearLayout
+            android:id="@+id/volume_dialog_floating_sliders_container"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+            android:gravity="bottom"
+            android:orientation="horizontal"
+            android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+            android:showDividers="middle" />
 
-        <include layout="@layout/volume_dialog_slider" />
+        <LinearLayout
+            android:id="@+id/volume_dialog"
+            android:layout_width="@dimen/volume_dialog_width"
+            android:layout_height="wrap_content"
+            android:background="@drawable/volume_dialog_background"
+            android:clipChildren="false"
+            android:clipToOutline="false"
+            android:clipToPadding="false"
+            android:divider="@drawable/volume_dialog_spacer"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"
+            android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+            android:showDividers="middle">
 
-        <ImageButton
-            android:id="@+id/volume_dialog_settings"
-            android:layout_width="@dimen/volume_dialog_button_size"
-            android:layout_height="@dimen/volume_dialog_button_size"
-            android:background="@drawable/ripple_drawable_20dp"
-            android:contentDescription="@string/accessibility_volume_settings"
-            android:soundEffectsEnabled="false"
-            android:src="@drawable/horizontal_ellipsis"
-            android:tint="?androidprv:attr/materialColorPrimary" />
+            <include layout="@layout/volume_ringer_drawer" />
+
+            <include layout="@layout/volume_dialog_slider" />
+
+            <ImageButton
+                android:id="@+id/volume_dialog_settings"
+                android:layout_width="@dimen/volume_dialog_button_size"
+                android:layout_height="@dimen/volume_dialog_button_size"
+                android:background="@drawable/ripple_drawable_20dp"
+                android:contentDescription="@string/accessibility_volume_settings"
+                android:soundEffectsEnabled="false"
+                android:src="@drawable/horizontal_ellipsis"
+                android:tint="?androidprv:attr/materialColorPrimary" />
+        </LinearLayout>
     </LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7d071cd..c69b98c 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -557,6 +557,16 @@
         <item name="android:showWhenLocked">true</item>
     </style>
 
+    <style name="Theme.SystemUI.Dialog.Volume">
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:showWhenLocked">true</item>
+        <item name="android:windowBackground">@color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowIsFloating">false</item>
+        <item name="android:windowNoTitle">true</item>
+    </style>
+
     <style name="SystemUI.Material3.Slider.Volume">
         <item name="trackHeight">40dp</item>
         <item name="thumbHeight">52dp</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 1342dd0..95830b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -15,6 +15,8 @@
  */
 package com.android.keyguard;
 
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
+
 import android.annotation.NonNull;
 import android.app.Presentation;
 import android.content.Context;
@@ -36,18 +38,24 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.views.NavigationBarView;
 import com.android.systemui.settings.DisplayTracker;
+import com.android.systemui.shade.data.repository.ShadePositionRepository;
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import dagger.Lazy;
 
+import kotlinx.coroutines.CoroutineScope;
+
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 @SysUISingleton
 public class KeyguardDisplayManager {
@@ -58,6 +66,7 @@
     private final DisplayManager mDisplayService;
     private final DisplayTracker mDisplayTracker;
     private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
+    private final Provider<ShadePositionRepository> mShadePositionRepositoryProvider;
     private final ConnectedDisplayKeyguardPresentation.Factory
             mConnectedDisplayKeyguardPresentationFactory;
     private final Context mContext;
@@ -102,9 +111,12 @@
             DeviceStateHelper deviceStateHelper,
             KeyguardStateController keyguardStateController,
             ConnectedDisplayKeyguardPresentation.Factory
-                    connectedDisplayKeyguardPresentationFactory) {
+                    connectedDisplayKeyguardPresentationFactory,
+            Provider<ShadePositionRepository> shadePositionRepositoryProvider,
+            @Application CoroutineScope appScope) {
         mContext = context;
         mNavigationBarControllerLazy = navigationBarControllerLazy;
+        mShadePositionRepositoryProvider = shadePositionRepositoryProvider;
         uiBgExecutor.execute(() -> mMediaRouter = mContext.getSystemService(MediaRouter.class));
         mDisplayService = mContext.getSystemService(DisplayManager.class);
         mDisplayTracker = displayTracker;
@@ -112,6 +124,17 @@
         mDeviceStateHelper = deviceStateHelper;
         mKeyguardStateController = keyguardStateController;
         mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory;
+        if (ShadeWindowGoesAround.isEnabled()) {
+            collectFlow(appScope, shadePositionRepositoryProvider.get().getDisplayId(),
+                    (id) -> onShadeWindowMovedToDisplayId(id));
+        }
+    }
+
+    private void onShadeWindowMovedToDisplayId(int shadeDisplayId) {
+        if (mShowing) {
+            hidePresentation(shadeDisplayId);
+            updateDisplays(/* showing= */ true);
+        }
     }
 
     private boolean isKeyguardShowable(Display display) {
@@ -119,9 +142,20 @@
             if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
             return false;
         }
-        if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
-            if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
-            return false;
+        if (ShadeWindowGoesAround.isEnabled()) {
+            int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue();
+            if (display.getDisplayId() == shadeDisplayId) {
+                if (DEBUG) {
+                    Log.i(TAG,
+                            "Do not show KeyguardPresentation on the shade window display");
+                }
+                return false;
+            }
+        } else {
+            if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
+                if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
+                return false;
+            }
         }
         display.getDisplayInfo(mTmpDisplayInfo);
         if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
new file mode 100644
index 0000000..8c393e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.model
+
+import android.graphics.drawable.Icon
+import android.hardware.input.InputGestureData
+import android.view.KeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+
+/**
+ * Internal Keyboard Shortcut models to use with [ShortcutCategoriesUtils.fetchShortcutCategory]
+ * when converting API models to Shortcut Helper Model [ShortcutCategory]. These Internal Models
+ * bridge the Gap between [InputGestureData] from custom shortcuts API and [KeyboardShortcutGroup]
+ * from default shortcuts API allowing us to have a single Utility Class that converts API models to
+ * Shortcut Helper models
+ *
+ * @param label Equivalent to shortcut helper's subcategory label
+ * @param items Keyboard Shortcuts received from API
+ * @param packageName package name of current app shortcut if available.
+ */
+data class InternalKeyboardShortcutGroup(
+    val label: String,
+    val items: List<InternalKeyboardShortcutInfo>,
+    val packageName: String? = null,
+)
+
+/**
+ * @param label Shortcut label
+ * @param keycode Key to trigger shortcut
+ * @param modifiers Mask of shortcut modifiers
+ * @param baseCharacter Key to trigger shortcut if is a character
+ * @param icon Shortcut icon if available - often used for app launch shortcuts
+ * @param isCustomShortcut If Shortcut is user customized or system defined.
+ */
+data class InternalKeyboardShortcutInfo(
+    val label: String,
+    val keycode: Int,
+    val modifiers: Int,
+    val baseCharacter: Char = Char.MIN_VALUE,
+    val icon: Icon? = null,
+    val isCustomShortcut: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..7f8fbb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
@@ -0,0 +1,175 @@
+/*
+ * 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 android.content.Context
+import android.content.Context.INPUT_SERVICE
+import android.hardware.input.InputGestureData
+import android.hardware.input.InputGestureData.KeyTrigger
+import android.hardware.input.InputManager
+import android.hardware.input.InputSettings
+import android.hardware.input.KeyGestureEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
+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.ShortcutHelperState.Active
+import com.android.systemui.settings.UserTracker
+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
+class CustomShortcutCategoriesRepository
+@Inject
+constructor(
+    stateRepository: ShortcutHelperStateRepository,
+    private val userTracker: UserTracker,
+    @Background private val backgroundScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
+) : ShortcutCategoriesRepository {
+
+    private val userContext: Context
+        get() = userTracker.createCurrentUserContext(userTracker.userContext)
+
+    // Input manager created with user context to provide correct user id when requesting custom
+    // shortcut
+    private val inputManager: InputManager
+        get() = userContext.getSystemService(INPUT_SERVICE) as InputManager
+
+    private val activeInputDevice =
+        stateRepository.state.map {
+            if (it is Active) {
+                withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
+            } else {
+                null
+            }
+        }
+
+    override val categories: Flow<List<ShortcutCategory>> =
+        activeInputDevice
+            .map { inputDevice ->
+                if (inputDevice == null) {
+                    emptyList()
+                } else {
+                    val customInputGesturesForUser: List<InputGestureData> =
+                        if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
+                            inputManager.getCustomInputGestures(/* filter= */ null)
+                        } else emptyList()
+                    val sources = toInternalGroupSources(customInputGesturesForUser)
+                    val supportedKeyCodes =
+                        shortcutCategoriesUtils.fetchSupportedKeyCodes(
+                            inputDevice.id,
+                            sources.map { it.groups },
+                        )
+
+                    val result =
+                        sources.mapNotNull { source ->
+                            shortcutCategoriesUtils.fetchShortcutCategory(
+                                type = source.type,
+                                groups = source.groups,
+                                inputDevice = inputDevice,
+                                supportedKeyCodes = supportedKeyCodes,
+                            )
+                        }
+                    result
+                }
+            }
+            .stateIn(
+                scope = backgroundScope,
+                initialValue = emptyList(),
+                started = SharingStarted.Lazily,
+            )
+
+    private fun toInternalGroupSources(
+        inputGestures: List<InputGestureData>
+    ): List<InternalGroupsSource> {
+        val ungroupedInternalGroupSources =
+            inputGestures.mapNotNull { gestureData ->
+                val keyTrigger = gestureData.trigger as KeyTrigger
+                val keyGestureType = gestureData.action.keyGestureType()
+                fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
+                    toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let {
+                        internalKeyboardShortcutInfo ->
+                        val group =
+                            InternalKeyboardShortcutGroup(
+                                label = groupLabel,
+                                items = listOf(internalKeyboardShortcutInfo),
+                            )
+
+                        fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
+                            InternalGroupsSource(groups = listOf(group), type = it)
+                        }
+                    }
+                }
+            }
+
+        return ungroupedInternalGroupSources
+    }
+
+    private fun toInternalKeyboardShortcutInfo(
+        keyGestureType: Int,
+        keyTrigger: KeyTrigger,
+    ): InternalKeyboardShortcutInfo? {
+        fetchShortcutInfoLabelByGestureType(keyGestureType)?.let {
+            return InternalKeyboardShortcutInfo(
+                label = it,
+                keycode = keyTrigger.keycode,
+                modifiers = keyTrigger.modifierState,
+                isCustomShortcut = true,
+            )
+        }
+        return null
+    }
+
+    private fun fetchGroupLabelByGestureType(
+        @KeyGestureEvent.KeyGestureType keyGestureType: Int
+    ): String? {
+        return InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap.getOrDefault(
+            keyGestureType,
+            null,
+        )
+    }
+
+    private fun fetchShortcutInfoLabelByGestureType(
+        @KeyGestureEvent.KeyGestureType keyGestureType: Int
+    ): String? {
+        return InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap.getOrDefault(
+            keyGestureType,
+            null,
+        )
+    }
+
+    private fun fetchShortcutCategoryTypeByGestureType(
+        @KeyGestureEvent.KeyGestureType keyGestureType: Int
+    ): ShortcutCategoryType? {
+        return InputGestures.gestureToShortcutCategoryTypeMap.getOrDefault(keyGestureType, null)
+    }
+
+    private data class InternalGroupsSource(
+        val groups: List<InternalKeyboardShortcutGroup>,
+        val type: ShortcutCategoryType,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..5bb7cdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt
@@ -0,0 +1,160 @@
+/*
+ * 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 android.hardware.input.InputManager
+import android.view.KeyboardShortcutGroup
+import android.view.KeyboardShortcutInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
+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.ShortcutCategory
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
+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.ShortcutHelperState.Active
+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
+class DefaultShortcutCategoriesRepository
+@Inject
+constructor(
+    @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,
+    shortcutCategoriesUtils: ShortcutCategoriesUtils,
+) : ShortcutCategoriesRepository {
+
+    private val sources =
+        listOf(
+            InternalGroupsSource(source = systemShortcutsSource, typeProvider = { System }),
+            InternalGroupsSource(
+                source = multitaskingShortcutsSource,
+                typeProvider = { MultiTasking },
+            ),
+            InternalGroupsSource(
+                source = appCategoriesShortcutsSource,
+                typeProvider = { AppCategories },
+            ),
+            InternalGroupsSource(
+                source = inputShortcutsSource,
+                typeProvider = { InputMethodEditor },
+            ),
+            InternalGroupsSource(
+                source = currentAppShortcutsSource,
+                typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) },
+            ),
+        )
+
+    private val activeInputDevice =
+        stateRepository.state.map {
+            if (it is Active) {
+                withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
+            } else {
+                null
+            }
+        }
+
+    override val categories: Flow<List<ShortcutCategory>> =
+        activeInputDevice
+            .map { inputDevice ->
+                if (inputDevice == null) {
+                    return@map emptyList()
+                }
+                val groupsFromAllSources =
+                    sources.map {
+                        toInternalKeyboardShortcutGroups(it.source.shortcutGroups(inputDevice.id))
+                    }
+                val supportedKeyCodes =
+                    shortcutCategoriesUtils.fetchSupportedKeyCodes(
+                        inputDevice.id,
+                        groupsFromAllSources,
+                    )
+                return@map sources.mapIndexedNotNull { index, internalGroupsSource ->
+                    shortcutCategoriesUtils.fetchShortcutCategory(
+                        internalGroupsSource.typeProvider(groupsFromAllSources[index]),
+                        groupsFromAllSources[index],
+                        inputDevice,
+                        supportedKeyCodes,
+                    )
+                }
+            }
+            .stateIn(
+                scope = backgroundScope,
+                started = SharingStarted.Lazily,
+                initialValue = emptyList(),
+            )
+
+    private fun toInternalKeyboardShortcutGroups(
+        keyboardShortcutGroups: List<KeyboardShortcutGroup>
+    ): List<InternalKeyboardShortcutGroup> {
+        return keyboardShortcutGroups.map { group ->
+            InternalKeyboardShortcutGroup(
+                label = group.label.toString(),
+                items = group.items.map { toInternalKeyboardShortcutInfo(it) },
+                packageName = group.packageName?.toString(),
+            )
+        }
+    }
+
+    private fun toInternalKeyboardShortcutInfo(
+        keyboardShortcutInfo: KeyboardShortcutInfo
+    ): InternalKeyboardShortcutInfo {
+        return InternalKeyboardShortcutInfo(
+            label = keyboardShortcutInfo.label!!.toString(),
+            keycode = keyboardShortcutInfo.keycode,
+            modifiers = keyboardShortcutInfo.modifiers,
+            baseCharacter = keyboardShortcutInfo.baseCharacter,
+            icon = keyboardShortcutInfo.icon,
+        )
+    }
+
+    private fun getCurrentAppShortcutCategoryType(
+        shortcutGroups: List<InternalKeyboardShortcutGroup>
+    ): ShortcutCategoryType? {
+        val packageName = shortcutGroups.firstOrNull()?.packageName ?: return null
+        return CurrentApp(packageName)
+    }
+
+    private class InternalGroupsSource(
+        val source: KeyboardShortcutGroupsSource,
+        val typeProvider: (groups: List<InternalKeyboardShortcutGroup>) -> ShortcutCategoryType?,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt
new file mode 100644
index 0000000..28134db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt
@@ -0,0 +1,156 @@
+/*
+ * 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 android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_BACK
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
+
+object InputGestures {
+    val gestureToShortcutCategoryTypeMap =
+        mapOf(
+            // System Category
+            KEY_GESTURE_TYPE_HOME to System,
+            KEY_GESTURE_TYPE_RECENT_APPS to System,
+            KEY_GESTURE_TYPE_BACK to System,
+            KEY_GESTURE_TYPE_TAKE_SCREENSHOT to System,
+            KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to System,
+            KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to System,
+            KEY_GESTURE_TYPE_LOCK_SCREEN to System,
+            KEY_GESTURE_TYPE_OPEN_NOTES to System,
+            KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to System,
+            KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System,
+            KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System,
+            KEY_GESTURE_TYPE_ALL_APPS to System,
+
+            // Multitasking Category
+            KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking,
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to MultiTasking,
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to MultiTasking,
+            KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to MultiTasking,
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to MultiTasking,
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to MultiTasking,
+
+            // App Category
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to AppCategories,
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to AppCategories,
+        )
+
+    // TODO move all string to to resources use the same resources as the original shortcuts
+    // - that way when the strings are translated there are no discrepancies
+    val gestureToInternalKeyboardShortcutGroupLabelMap =
+        mapOf(
+            // System Category
+            KEY_GESTURE_TYPE_HOME to "System controls",
+            KEY_GESTURE_TYPE_RECENT_APPS to "System controls",
+            KEY_GESTURE_TYPE_BACK to "System controls",
+            KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "System controls",
+            KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "System controls",
+            KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "System controls",
+            KEY_GESTURE_TYPE_LOCK_SCREEN to "System controls",
+            KEY_GESTURE_TYPE_ALL_APPS to "System controls",
+            KEY_GESTURE_TYPE_OPEN_NOTES to "System apps",
+            KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "System apps",
+            KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "System apps",
+            KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "System apps",
+
+            // Multitasking Category
+            KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Recent apps",
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to "Split screen",
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to "Split screen",
+            KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Split screen",
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to "Split screen",
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to "Split screen",
+
+            // App Category
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Applications",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Applications",
+        )
+
+    val gestureToInternalKeyboardShortcutInfoLabelMap =
+        mapOf(
+            // System Category
+            KEY_GESTURE_TYPE_HOME to "Go to home screen",
+            KEY_GESTURE_TYPE_RECENT_APPS to "View recent apps",
+            KEY_GESTURE_TYPE_BACK to "Go back",
+            KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "Take screenshot",
+            KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "Show shortcuts",
+            KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "View notifications",
+            KEY_GESTURE_TYPE_LOCK_SCREEN to "Lock screen",
+            KEY_GESTURE_TYPE_ALL_APPS to "Open apps list",
+            KEY_GESTURE_TYPE_OPEN_NOTES to "Take a note",
+            KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "Open settings",
+            KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "Open assistant",
+            KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "Open assistant",
+
+            // Multitasking Category
+            KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Cycle forward through recent apps",
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to
+                "Use split screen with current app on the left",
+            KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to
+                "Use split screen with current app on the right",
+            KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Switch from split screen to full screen",
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to
+                "Switch to app on left or above while using split screen",
+            KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to
+                "Switch to app on right or below while using split screen",
+
+            // App Category
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Calculator",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Calendar",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Chrome",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Contacts",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Gmail",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Maps",
+            KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Messages",
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt
new file mode 100644
index 0000000..2e8cc00
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
+import kotlinx.coroutines.flow.Flow
+
+interface ShortcutCategoriesRepository {
+    val categories: Flow<List<ShortcutCategory>>
+}
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/ShortcutCategoriesUtils.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
rename to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
index 12dd581..899fd15 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/ShortcutCategoriesUtils.kt
@@ -24,123 +24,34 @@
 import android.view.InputDevice
 import android.view.KeyCharacterMap
 import android.view.KeyEvent
-import android.view.KeyboardShortcutGroup
-import android.view.KeyboardShortcutInfo
 import com.android.systemui.Flags.shortcutHelperKeyGlyph
-import com.android.systemui.dagger.SysUISingleton
 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.data.model.InternalKeyboardShortcutGroup
+import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
 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.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
 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
 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 kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.withContext
 
-@SysUISingleton
-class ShortcutHelperCategoriesRepository
+class ShortcutCategoriesUtils
 @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,
+    @Background private val backgroundCoroutineContext: CoroutineContext,
     private val inputManager: InputManager,
-    stateRepository: ShortcutHelperStateRepository,
 ) {
-
-    private val sources =
-        listOf(
-            InternalGroupsSource(
-                source = systemShortcutsSource,
-                isTrusted = true,
-                typeProvider = { System },
-            ),
-            InternalGroupsSource(
-                source = multitaskingShortcutsSource,
-                isTrusted = true,
-                typeProvider = { MultiTasking },
-            ),
-            InternalGroupsSource(
-                source = appCategoriesShortcutsSource,
-                isTrusted = true,
-                typeProvider = { AppCategories },
-            ),
-            InternalGroupsSource(
-                source = inputShortcutsSource,
-                isTrusted = false,
-                typeProvider = { InputMethodEditor },
-            ),
-            InternalGroupsSource(
-                source = currentAppShortcutsSource,
-                isTrusted = false,
-                typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) },
-            ),
-        )
-
-    private val activeInputDevice =
-        stateRepository.state.map {
-            if (it is Active) {
-                withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) }
-            } else {
-                null
-            }
-        }
-
-    val categories: Flow<List<ShortcutCategory>> =
-        activeInputDevice
-            .map { inputDevice ->
-                if (inputDevice == null) {
-                    return@map emptyList()
-                }
-                val groupsFromAllSources = sources.map { it.source.shortcutGroups(inputDevice.id) }
-                val supportedKeyCodes = fetchSupportedKeyCodes(inputDevice.id, groupsFromAllSources)
-                return@map sources.mapIndexedNotNull { index, internalGroupsSource ->
-                    fetchShortcutCategory(
-                        internalGroupsSource,
-                        groupsFromAllSources[index],
-                        inputDevice,
-                        supportedKeyCodes,
-                    )
-                }
-            }
-            .stateIn(
-                scope = backgroundScope,
-                started = SharingStarted.Lazily,
-                initialValue = emptyList(),
-            )
-
-    private fun fetchShortcutCategory(
-        internalGroupsSource: InternalGroupsSource,
-        groups: List<KeyboardShortcutGroup>,
+    fun fetchShortcutCategory(
+        type: ShortcutCategoryType?,
+        groups: List<InternalKeyboardShortcutGroup>,
         inputDevice: InputDevice,
         supportedKeyCodes: Set<Int>,
     ): ShortcutCategory? {
-        val type = internalGroupsSource.typeProvider(groups)
         return if (type == null) {
             null
         } else {
@@ -151,27 +62,17 @@
                 inputDevice.keyCharacterMap,
                 type,
                 groups,
-                internalGroupsSource.isTrusted,
+                type.isTrusted,
                 supportedKeyCodes,
             )
         }
     }
 
-    private fun getCurrentAppShortcutCategoryType(
-        shortcutGroups: List<KeyboardShortcutGroup>
-    ): ShortcutCategoryType? {
-        return if (shortcutGroups.isEmpty()) {
-            null
-        } else {
-            CurrentApp(packageName = shortcutGroups[0].packageName.toString())
-        }
-    }
-
     private fun toShortcutCategory(
         keyGlyphMap: KeyGlyphMap?,
         keyCharacterMap: KeyCharacterMap,
         type: ShortcutCategoryType,
-        shortcutGroups: List<KeyboardShortcutGroup>,
+        shortcutGroups: List<InternalKeyboardShortcutGroup>,
         keepIcons: Boolean,
         supportedKeyCodes: Set<Int>,
     ): ShortcutCategory? {
@@ -179,7 +80,7 @@
             shortcutGroups
                 .map { shortcutGroup ->
                     ShortcutSubCategory(
-                        shortcutGroup.label.toString(),
+                        shortcutGroup.label,
                         toShortcuts(
                             keyGlyphMap,
                             keyCharacterMap,
@@ -201,7 +102,7 @@
     private fun toShortcuts(
         keyGlyphMap: KeyGlyphMap?,
         keyCharacterMap: KeyCharacterMap,
-        infoList: List<KeyboardShortcutInfo>,
+        infoList: List<InternalKeyboardShortcutInfo>,
         keepIcons: Boolean,
         supportedKeyCodes: Set<Int>,
     ) =
@@ -216,13 +117,13 @@
     private fun toShortcut(
         keyGlyphMap: KeyGlyphMap?,
         keyCharacterMap: KeyCharacterMap,
-        shortcutInfo: KeyboardShortcutInfo,
+        shortcutInfo: InternalKeyboardShortcutInfo,
         keepIcon: Boolean,
     ): Shortcut? {
         val shortcutCommand =
             toShortcutCommand(keyGlyphMap, keyCharacterMap, shortcutInfo) ?: return null
         return Shortcut(
-            label = shortcutInfo.label!!.toString(),
+            label = shortcutInfo.label,
             icon = toShortcutIcon(keepIcon, shortcutInfo),
             commands = listOf(shortcutCommand),
         )
@@ -230,7 +131,7 @@
 
     private fun toShortcutIcon(
         keepIcon: Boolean,
-        shortcutInfo: KeyboardShortcutInfo,
+        shortcutInfo: InternalKeyboardShortcutInfo,
     ): ShortcutIcon? {
         if (!keepIcon) {
             return null
@@ -247,7 +148,7 @@
     private fun toShortcutCommand(
         keyGlyphMap: KeyGlyphMap?,
         keyCharacterMap: KeyCharacterMap,
-        info: KeyboardShortcutInfo,
+        info: InternalKeyboardShortcutInfo,
     ): ShortcutCommand? {
         val keys = mutableListOf<ShortcutKey>()
         var remainingModifiers = info.modifiers
@@ -272,7 +173,7 @@
             Log.wtf(TAG, "No keys for $info")
             return null
         }
-        return ShortcutCommand(keys)
+        return ShortcutCommand(keys = keys, isCustom = info.isCustomShortcut)
     }
 
     private fun toShortcutModifierKey(keyGlyphMap: KeyGlyphMap?, modifierMask: Int): ShortcutKey? {
@@ -325,11 +226,11 @@
         return null
     }
 
-    private suspend fun fetchSupportedKeyCodes(
+    suspend fun fetchSupportedKeyCodes(
         deviceId: Int,
-        groupsFromAllSources: List<List<KeyboardShortcutGroup>>,
+        groupsFromAllSources: List<List<InternalKeyboardShortcutGroup>>,
     ): Set<Int> =
-        withContext(backgroundDispatcher) {
+        withContext(backgroundCoroutineContext) {
             val allUsedKeyCodes =
                 groupsFromAllSources
                     .flatMap { groups -> groups.flatMap { group -> group.items } }
@@ -342,14 +243,8 @@
                 .toSet()
         }
 
-    private class InternalGroupsSource(
-        val source: KeyboardShortcutGroupsSource,
-        val isTrusted: Boolean,
-        val typeProvider: (groups: List<KeyboardShortcutGroup>) -> ShortcutCategoryType?,
-    )
-
     companion object {
-        private const val TAG = "SHCategoriesRepo"
+        private const val TAG = "ShortcutCategoriesUtils"
 
         private val SUPPORTED_MODIFIERS =
             listOf(
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 6f19561..39fc27d 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
@@ -17,7 +17,7 @@
 package com.android.systemui.keyboard.shortcut.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
 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.ShortcutSubCategory
@@ -28,9 +28,7 @@
 @SysUISingleton
 class ShortcutHelperCategoriesInteractor
 @Inject
-constructor(
-    categoriesRepository: ShortcutHelperCategoriesRepository,
-) {
+constructor(categoriesRepository: DefaultShortcutCategoriesRepository) {
 
     val shortcutCategories: Flow<List<ShortcutCategory>> =
         categoriesRepository.categories.map { categories ->
@@ -42,12 +40,12 @@
             shortcutCategory.subCategories.map {
                 ShortcutSubCategory(
                     label = it.label,
-                    shortcuts = groupShortcutsInSubcategory(it.shortcuts)
+                    shortcuts = groupShortcutsInSubcategory(it.shortcuts),
                 )
             }
         return ShortcutCategory(
             type = shortcutCategory.type,
-            subCategories = subCategoriesWithGroupedShortcuts
+            subCategories = subCategoriesWithGroupedShortcuts,
         )
     }
 
@@ -59,7 +57,7 @@
                 Shortcut(
                     label = commonLabel,
                     icon = groupedShortcuts.firstOrNull()?.icon,
-                    commands = groupedShortcuts.flatMap { it.commands }
+                    commands = groupedShortcuts.flatMap { it.commands },
                 )
             }
 }
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 c89ef15..813a1fca 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
@@ -17,24 +17,36 @@
 package com.android.systemui.keyboard.shortcut.shared.model
 
 sealed interface ShortcutCategoryType {
-    data object System : ShortcutCategoryType
+    val isTrusted: Boolean
 
-    data object MultiTasking : ShortcutCategoryType
+    data object System : ShortcutCategoryType {
+        override val isTrusted: Boolean = true
+    }
 
-    data object InputMethodEditor : ShortcutCategoryType
+    data object MultiTasking : ShortcutCategoryType {
+        override val isTrusted: Boolean = true
+    }
 
-    data object AppCategories : ShortcutCategoryType
+    data object InputMethodEditor : ShortcutCategoryType {
+        override val isTrusted: Boolean = false
+    }
 
-    data class CurrentApp(val packageName: String) : ShortcutCategoryType
+    data object AppCategories : ShortcutCategoryType {
+        override val isTrusted: Boolean = true
+    }
+
+    data class CurrentApp(val packageName: String) : ShortcutCategoryType {
+        override val isTrusted: Boolean = false
+    }
 }
 
 data class ShortcutCategory(
     val type: ShortcutCategoryType,
-    val subCategories: List<ShortcutSubCategory>
+    val subCategories: List<ShortcutSubCategory>,
 ) {
     constructor(
         type: ShortcutCategoryType,
-        vararg subCategories: ShortcutSubCategory
+        vararg subCategories: ShortcutSubCategory,
     ) : this(type, subCategories.asList())
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
index 28451ae..c7e6b43 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt
@@ -18,10 +18,11 @@
 
 import androidx.annotation.DrawableRes
 
-data class ShortcutCommand(val keys: List<ShortcutKey>)
+data class ShortcutCommand(val keys: List<ShortcutKey>, val isCustom: Boolean = false)
 
 class ShortcutCommandBuilder {
     private val keys = mutableListOf<ShortcutKey>()
+    private var isCustom = false
 
     fun key(text: String) {
         keys += ShortcutKey.Text(text)
@@ -31,7 +32,11 @@
         keys += ShortcutKey.Icon.ResIdIcon(drawableResId)
     }
 
-    fun build() = ShortcutCommand(keys)
+    fun isCustom(isCustom: Boolean) {
+        this.isCustom = isCustom
+    }
+
+    fun build() = ShortcutCommand(keys, isCustom)
 }
 
 fun shortcutCommand(block: ShortcutCommandBuilder.() -> Unit) =
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
index 807c70b..10a201e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
@@ -36,6 +37,7 @@
 import com.android.systemui.keyboard.shortcut.ui.composable.getWidth
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.statusbar.phone.createBottomSheet
 import javax.inject.Inject
@@ -86,6 +88,7 @@
                     },
                 )
                 dialog.setOnDismissListener { shortcutHelperViewModel.onViewClosed() }
+                dialog.setTitle(stringResource(R.string.shortcut_helper_title))
             },
             maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt b/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
index 6aaa27d..eca4051 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/UserAwareConnection.kt
@@ -69,7 +69,11 @@
     @WorkerThread
     fun doUnBind() {
         if (shouldUnBind) {
-            userContextProvider.userContext.unbindService(this)
+            try {
+                userContextProvider.userContext.unbindService(this)
+            } catch (e: IllegalArgumentException) {
+                Log.e(TAG, "Can't disconnect because service wasn't connected anyways.", e)
+            }
             shouldUnBind = false
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 63510b8..e15830e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -157,7 +157,6 @@
 
     @SysUISingleton
     @Provides
-    @ShadeDisplayAware
     fun provideShadePositionRepository(impl: ShadePositionRepositoryImpl): ShadePositionRepository {
         ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
         return impl
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt
new file mode 100644
index 0000000..37210b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadePositionRepository.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.shade.data.repository
+
+import android.view.Display
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeShadePositionRepository : ShadePositionRepository {
+    private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+
+    override fun setDisplayId(displayId: Int) {
+        _displayId.value = displayId
+    }
+
+    override val displayId: StateFlow<Int>
+        get() = _displayId
+
+    override fun resetDisplayId() {
+        _displayId.value = Display.DEFAULT_DISPLAY
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
index 6f492cf..c23ff53 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt
@@ -32,7 +32,7 @@
 
     /** Is the refactor enabled */
     @JvmStatic
-    inline val isEnabled
+    inline val isEnabled: Boolean
         get() = Flags.shadeWindowGoesAround()
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 564d52a..1cb4c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -27,7 +27,10 @@
 interface SystemStatusAnimationScheduler :
     CallbackController<SystemStatusAnimationCallback>, Dumpable {
 
-    /** StateFlow holding the current [SystemEventAnimationState] at any time. */
+    /**
+     * The current state of the animation. This can be used from compose functions to coordinate
+     * their animations with the chip
+     */
     val animationState: StateFlow<SystemEventAnimationState>
 
     fun onStatusEvent(event: StatusEvent)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt
new file mode 100644
index 0000000..971f5d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.statusbar.events.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+/** Repository to expose the [SystemStatusAnimationScheduler] state via flows */
+interface SystemStatusEventAnimationRepository {
+    val animationState: StateFlow<SystemEventAnimationState>
+}
+
+@SysUISingleton
+class SystemStatusEventAnimationRepositoryImpl
+@Inject
+constructor(scheduler: SystemStatusAnimationScheduler) : SystemStatusEventAnimationRepository {
+    override val animationState = scheduler.animationState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.kt
new file mode 100644
index 0000000..3e30642
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractor.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.statusbar.events.domain.interactor
+
+import android.view.View
+import androidx.core.animation.Animator
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepository
+import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventDefaultAnimator
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Interactor for dealing with system status event animations. This class can be used to monitor the
+ * current [animationState], and defines some common animation functions that an handle hiding
+ * system chrome in order to make space for the event chips
+ */
+@SysUISingleton
+class SystemStatusEventAnimationInteractor
+@Inject
+constructor(
+    repo: SystemStatusEventAnimationRepository,
+    configurationInteractor: ConfigurationInteractor,
+    @Application scope: CoroutineScope,
+) {
+    private val chipAnimateInTranslationX =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.ongoing_appops_chip_animation_in_status_bar_translation_x)
+            .stateIn(scope, SharingStarted.Eagerly, 0)
+
+    private val chipAnimateOutTranslationX =
+        configurationInteractor
+            .dimensionPixelSize(R.dimen.ongoing_appops_chip_animation_out_status_bar_translation_x)
+            .stateIn(scope, SharingStarted.Eagerly, 0)
+
+    val animationState = repo.animationState
+
+    private fun getDefaultStatusBarAnimationForChipEnter(
+        setX: (Float) -> Unit,
+        setAlpha: (Float) -> Unit,
+    ): Animator {
+        return StatusBarSystemEventDefaultAnimator.getDefaultStatusBarAnimationForChipEnter(
+            chipAnimateInTranslationX.value,
+            setX,
+            setAlpha,
+        )
+    }
+
+    private fun getDefaultStatusBarAnimationForChipExit(
+        setX: (Float) -> Unit,
+        setAlpha: (Float) -> Unit,
+    ): Animator {
+        return StatusBarSystemEventDefaultAnimator.getDefaultStatusBarAnimationForChipExit(
+            chipAnimateOutTranslationX.value,
+            setX,
+            setAlpha,
+        )
+    }
+
+    fun animateStatusBarContentForChipEnter(v: View) {
+        getDefaultStatusBarAnimationForChipEnter(setX = v::setTranslationX, setAlpha = v::setAlpha)
+            .start()
+    }
+
+    fun animateStatusBarContentForChipExit(v: View) {
+        v.translationX = chipAnimateOutTranslationX.value.toFloat()
+        getDefaultStatusBarAnimationForChipExit(setX = v::setTranslationX, setAlpha = v::setAlpha)
+            .start()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index d991b1d..41db5f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -174,6 +174,14 @@
     private val dumpableName = TAG + nameSuffix
     private val commandName = StatusBarInsetsCommand.NAME + nameSuffix
 
+    init {
+        if (!StatusBarConnectedDisplays.isEnabled) {
+            // Call start(), since it is not called when the flag is disabled, to keep the old
+            // behavior as it was.
+            start()
+        }
+    }
+
     override fun start() {
         configurationController.addCallback(this)
         dumpManager.registerDumpable(dumpableName, this)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index c55a63c..23b4b65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -376,7 +376,11 @@
         mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
 
         mHomeStatusBarViewBinder.bind(
-                mStatusBar, mHomeStatusBarViewModel, mStatusBarVisibilityChangeListener);
+                mStatusBar,
+                mHomeStatusBarViewModel,
+                /* systemEventChipAnimateIn */ null,
+                /* systemEventChipAnimateOut */ null,
+                mStatusBarVisibilityChangeListener);
     }
 
     private String getDumpableName() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
index 1f9ea08..fd7bce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarSystemEventAnimator.kt
@@ -71,49 +71,87 @@
 
     override fun onSystemEventAnimationBegin(): Animator {
         isAnimationRunning = true
-        val moveOut =
-            ValueAnimator.ofFloat(0f, 1f).apply {
-                duration = 23.frames
-                interpolator = STATUS_BAR_X_MOVE_OUT
-                addUpdateListener {
-                    onTranslationXChanged(-(translationXIn * animatedValue as Float))
-                }
-            }
-        val alphaOut =
-            ValueAnimator.ofFloat(1f, 0f).apply {
-                duration = 8.frames
-                interpolator = null
-                addUpdateListener { onAlphaChanged(animatedValue as Float) }
-            }
-
-        val animSet = AnimatorSet()
-        animSet.playTogether(moveOut, alphaOut)
-        return animSet
+        return getDefaultStatusBarAnimationForChipEnter(
+            translationXIn,
+            onTranslationXChanged,
+            onAlphaChanged,
+        )
     }
 
     override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
         onTranslationXChanged(translationXOut.toFloat())
-        val moveIn =
-            ValueAnimator.ofFloat(1f, 0f).apply {
-                duration = 23.frames
-                startDelay = 7.frames
-                interpolator = STATUS_BAR_X_MOVE_IN
-                addUpdateListener {
-                    onTranslationXChanged(translationXOut * animatedValue as Float)
-                }
-            }
-        val alphaIn =
-            ValueAnimator.ofFloat(0f, 1f).apply {
-                duration = 5.frames
-                startDelay = 11.frames
-                interpolator = null
-                addUpdateListener { onAlphaChanged(animatedValue as Float) }
-            }
+        val anim =
+            getDefaultStatusBarAnimationForChipExit(
+                translationXOut,
+                onTranslationXChanged,
+                onAlphaChanged,
+            )
+        anim.doOnEnd { isAnimationRunning = false }
+        anim.doOnCancel { isAnimationRunning = false }
 
-        val animatorSet = AnimatorSet()
-        animatorSet.playTogether(moveIn, alphaIn)
-        animatorSet.doOnEnd { isAnimationRunning = false }
-        animatorSet.doOnCancel { isAnimationRunning = false }
-        return animatorSet
+        return anim
+    }
+
+    /** Static definition of these animations so we can use them more easily from view binders */
+    companion object {
+        /**
+         * Chip: coming in. Animated view: going out.
+         *
+         * Implements the exact spec for animating any status bar elements OUT to make space for the
+         * chip IN animation.
+         */
+        fun getDefaultStatusBarAnimationForChipEnter(
+            targetTranslation: Int,
+            setX: (Float) -> Unit,
+            setAlpha: (Float) -> Unit,
+        ): Animator {
+            val moveOut =
+                ValueAnimator.ofFloat(0f, 1f).apply {
+                    duration = 23.frames
+                    interpolator = STATUS_BAR_X_MOVE_OUT
+                    addUpdateListener { setX(-(targetTranslation * animatedValue as Float)) }
+                }
+            val alphaOut =
+                ValueAnimator.ofFloat(1f, 0f).apply {
+                    duration = 8.frames
+                    interpolator = null
+                    addUpdateListener { setAlpha(animatedValue as Float) }
+                }
+
+            val animSet = AnimatorSet()
+            animSet.playTogether(moveOut, alphaOut)
+            return animSet
+        }
+
+        /**
+         * Chip: going out. Animated view: coming in.
+         *
+         * Implements the exact spec for animating any status bar elements IN as the chip is
+         * animating OUT
+         */
+        fun getDefaultStatusBarAnimationForChipExit(
+            targetTranslation: Int,
+            setX: (Float) -> Unit,
+            setAlpha: (Float) -> Unit,
+        ): Animator {
+            val moveIn =
+                ValueAnimator.ofFloat(1f, 0f).apply {
+                    duration = 23.frames
+                    startDelay = 7.frames
+                    interpolator = STATUS_BAR_X_MOVE_IN
+                    addUpdateListener { setX(targetTranslation * animatedValue as Float) }
+                }
+            val alphaIn =
+                ValueAnimator.ofFloat(0f, 1f).apply {
+                    duration = 5.frames
+                    startDelay = 11.frames
+                    interpolator = null
+                    addUpdateListener { setAlpha(animatedValue as Float) }
+                }
+
+            val animatorSet = AnimatorSet()
+            animatorSet.playTogether(moveIn, alphaIn)
+            return animatorSet
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 935b101..bfdc8bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -23,6 +23,8 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepository
+import com.android.systemui.statusbar.events.data.repository.SystemStatusEventAnimationRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -85,6 +87,11 @@
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
     @Binds
+    abstract fun systemStatusEventAnimationRepository(
+        impl: SystemStatusEventAnimationRepositoryImpl
+    ): SystemStatusEventAnimationRepository
+
+    @Binds
     abstract fun realDeviceBasedSatelliteRepository(
         impl: DeviceBasedSatelliteRepositoryImpl
     ): RealDeviceBasedSatelliteRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 2177e02..72df027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -32,6 +32,10 @@
 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel
@@ -46,10 +50,16 @@
     /**
      * Binds the view to the view-model. [listener] will be notified whenever an event that may
      * change the status bar visibility occurs.
+     *
+     * Null chip animations are used when [StatusBarRootModernization] is off (i.e., when we are
+     * binding from the fragment). If non-null, they control the animation of the system icon area
+     * to support the chip animations.
      */
     fun bind(
         view: View,
         viewModel: HomeStatusBarViewModel,
+        systemEventChipAnimateIn: ((View) -> Unit)?,
+        systemEventChipAnimateOut: ((View) -> Unit)?,
         listener: StatusBarVisibilityChangeListener,
     )
 }
@@ -59,6 +69,8 @@
     override fun bind(
         view: View,
         viewModel: HomeStatusBarViewModel,
+        systemEventChipAnimateIn: ((View) -> Unit)?,
+        systemEventChipAnimateOut: ((View) -> Unit)?,
         listener: StatusBarVisibilityChangeListener,
     ) {
         view.repeatWhenAttached {
@@ -95,6 +107,7 @@
                                 when (primaryChipModel) {
                                     is OngoingActivityChipModel.Shown ->
                                         primaryChipView.show(shouldAnimateChange = true)
+
                                     is OngoingActivityChipModel.Hidden ->
                                         primaryChipView.hide(
                                             state = View.GONE,
@@ -109,6 +122,7 @@
                                             hasSecondaryOngoingActivity = false,
                                             shouldAnimate = true,
                                         )
+
                                     is OngoingActivityChipModel.Hidden ->
                                         listener.onOngoingActivityStatusChanged(
                                             hasPrimaryOngoingActivity = false,
@@ -175,10 +189,30 @@
                         view.requireViewById<View>(R.id.status_bar_end_side_content)
                     // TODO(b/364360986): Also handle operator name view.
                     launch {
-                        viewModel.isSystemInfoVisible.collect {
-                            systemInfoView.adjustVisibility(it)
-                            // TODO(b/364360986): The system info view has a custom alpha controller
-                            // in CollapsedStatusBarFragment.
+                        viewModel.systemInfoCombinedVis.collect { (baseVis, animState) ->
+                            // Broadly speaking, the baseVis controls the view.visibility, and
+                            // the animation state uses only alpha to achieve its effect. This
+                            // means that we can always modify the visibility, and if we're
+                            // animating we can use the animState to handle it. If we are not
+                            // animating, then we can use the baseVis default animation
+                            if (animState.isAnimatingChip()) {
+                                // Just apply the visibility of the view, but don't animate
+                                systemInfoView.visibility = baseVis.visibility
+                                // Now apply the animation state, with its animator
+                                when (animState) {
+                                    AnimatingIn -> {
+                                        systemEventChipAnimateIn?.invoke(systemInfoView)
+                                    }
+                                    AnimatingOut -> {
+                                        systemEventChipAnimateOut?.invoke(systemInfoView)
+                                    }
+                                    else -> {
+                                        // Nothing to do here
+                                    }
+                                }
+                            } else {
+                                systemInfoView.adjustVisibility(baseVis)
+                            }
                         }
                     }
                 }
@@ -186,6 +220,14 @@
         }
     }
 
+    private fun SystemEventAnimationState.isAnimatingChip() =
+        when (this) {
+            AnimatingIn,
+            AnimatingOut,
+            RunningChipAnim -> true
+            else -> false
+        }
+
     private fun OngoingActivityChipModel.toVisibilityModel(): VisibilityModel {
         return VisibilityModel(
             visibility = if (this is OngoingActivityChipModel.Shown) View.VISIBLE else View.GONE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
index a9c2f72..1faa9f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore
+import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.phone.PhoneStatusBarView
@@ -59,6 +60,7 @@
     private val iconController: StatusBarIconController,
     private val ongoingCallController: OngoingCallController,
     private val darkIconDispatcherStore: DarkIconDispatcherStore,
+    private val eventAnimationInteractor: SystemStatusEventAnimationInteractor,
 ) {
     fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView {
         val composeView = ComposeView(root.context)
@@ -73,6 +75,7 @@
                     iconController = iconController,
                     ongoingCallController = ongoingCallController,
                     darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId),
+                    eventAnimationInteractor = eventAnimationInteractor,
                     onViewCreated = andThen,
                 )
             }
@@ -102,6 +105,7 @@
     iconController: StatusBarIconController,
     ongoingCallController: OngoingCallController,
     darkIconDispatcher: DarkIconDispatcher,
+    eventAnimationInteractor: SystemStatusEventAnimationInteractor,
     onViewCreated: (ViewGroup) -> Unit,
 ) {
     // None of these methods are used when [StatusBarRootModernization] is on.
@@ -181,6 +185,8 @@
                         statusBarViewBinder.bind(
                             phoneStatusBarView,
                             statusBarViewModel,
+                            eventAnimationInteractor::animateStatusBarContentForChipEnter,
+                            eventAnimationInteractor::animateStatusBarContentForChipExit,
                             nopVisibilityChangeListener,
                         )
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
index 4277a8b..6a9b43c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt
@@ -35,6 +35,9 @@
 import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel
+import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor
@@ -95,7 +98,12 @@
 
     val isClockVisible: Flow<VisibilityModel>
     val isNotificationIconContainerVisible: Flow<VisibilityModel>
-    val isSystemInfoVisible: Flow<VisibilityModel>
+    /**
+     * Pair of (system info visibility, event animation state). The animation state can be used to
+     * respond to the system event chip animations. In all cases, system info visibility correctly
+     * models the View.visibility for the system info area
+     */
+    val systemInfoCombinedVis: StateFlow<SystemInfoCombinedVisibilityModel>
 
     /**
      * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where
@@ -114,6 +122,12 @@
         /** True if a visibility change should be animated. */
         val shouldAnimateChange: Boolean,
     )
+
+    /** The combined visibility + animation state for the system info status bar area */
+    data class SystemInfoCombinedVisibilityModel(
+        val baseVisibility: VisibilityModel,
+        val animationState: SystemEventAnimationState,
+    )
 }
 
 @SysUISingleton
@@ -129,6 +143,7 @@
     sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor,
     shadeInteractor: ShadeInteractor,
     ongoingActivityChipsViewModel: OngoingActivityChipsViewModel,
+    animations: SystemStatusEventAnimationInteractor,
     @Application coroutineScope: CoroutineScope,
 ) : HomeStatusBarViewModel {
     override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> =
@@ -228,7 +243,7 @@
                 visibilityViaDisableFlags.animate,
             )
         }
-    override val isSystemInfoVisible: Flow<VisibilityModel> =
+    private val isSystemInfoVisible =
         combine(
             shouldHomeStatusBarBeVisible,
             collapsedStatusBarInteractor.visibilityViaDisableFlags,
@@ -238,6 +253,22 @@
             VisibilityModel(showSystemInfo.toVisibilityInt(), visibilityViaDisableFlags.animate)
         }
 
+    override val systemInfoCombinedVis =
+        combine(isSystemInfoVisible, animations.animationState) { sysInfoVisible, animationState ->
+                HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+                    sysInfoVisible,
+                    animationState,
+                )
+            }
+            .stateIn(
+                coroutineScope,
+                SharingStarted.WhileSubscribed(),
+                HomeStatusBarViewModel.SystemInfoCombinedVisibilityModel(
+                    VisibilityModel(View.VISIBLE, false),
+                    Idle,
+                ),
+            )
+
     @View.Visibility
     private fun Boolean.toVisibilityInt(): Int {
         return if (this) View.VISIBLE else View.GONE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
index b79d39e..36d64a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
@@ -39,7 +39,7 @@
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
 
 /**
- * Status bar view.
+ * Status bar view
  * We now extend WindowRootView so that we can host Compose views
  */
 public class StatusBarWindowView extends FrameLayout {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 4fc9a7c..5c0cc81 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -18,9 +18,13 @@
 
 import android.app.Dialog
 import android.content.Context
+import android.graphics.PixelFormat
 import android.os.Bundle
 import android.view.MotionEvent
+import android.view.ViewGroup
+import android.view.WindowManager
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
 import com.android.systemui.volume.Events
 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
 import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder
@@ -32,10 +36,34 @@
     @Application context: Context,
     private val viewBinder: VolumeDialogViewBinder,
     private val visibilityInteractor: VolumeDialogVisibilityInteractor,
-) : Dialog(context) {
+) : Dialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
+
+    init {
+        with(window!!) {
+            addFlags(
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+                    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+            )
+            addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+            setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+            setWindowAnimations(-1)
+            setFormat(PixelFormat.TRANSLUCENT)
+
+            attributes =
+                attributes.apply {
+                    title = "VolumeDialog" // Not the same as Window#setTitle
+                }
+            setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+        }
+        setCanceledOnTouchOutside(true)
+    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        setContentView(R.layout.volume_dialog)
         viewBinder.bind(this)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 78eabb2..d9a945c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -17,21 +17,22 @@
 package com.android.systemui.volume.dialog.ui.binder
 
 import android.app.Dialog
-import android.graphics.Color
-import android.graphics.PixelFormat
-import android.graphics.drawable.ColorDrawable
+import android.graphics.Rect
+import android.graphics.Region
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
-import android.view.Window
-import android.view.WindowManager
+import android.view.ViewTreeObserver
+import android.view.ViewTreeObserver.InternalInsetsInfo
+import android.widget.FrameLayout
+import androidx.annotation.GravityInt
 import com.android.internal.view.RotationPolicy
 import com.android.systemui.lifecycle.WindowLifecycleState
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.lifecycle.viewModel
 import com.android.systemui.res.R
+import com.android.systemui.util.children
 import com.android.systemui.volume.SystemUIInterpolators
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
 import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder
 import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder
@@ -52,6 +53,8 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 /** Binds the root view of the Volume Dialog. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -61,65 +64,41 @@
 constructor(
     private val volumeResources: VolumeDialogResources,
     private val gravityViewModel: VolumeDialogGravityViewModel,
-    private val viewModelFactory: VolumeDialogViewModel.Factory,
+    private val dialogViewModelFactory: VolumeDialogViewModel.Factory,
     private val jankListenerFactory: JankListenerFactory,
     private val tracer: VolumeTracer,
-    @VolumeDialog private val coroutineScope: CoroutineScope,
     private val volumeDialogRingerViewBinder: VolumeDialogRingerViewBinder,
     private val slidersViewBinder: VolumeDialogSlidersViewBinder,
     private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder,
 ) {
 
     fun bind(dialog: Dialog) {
-        setupDialog(dialog)
-        val view: View = dialog.requireViewById(R.id.volume_dialog_container)
-        view.alpha = 0f
-        view.repeatWhenAttached {
-            view.viewModel(
+        // Root view of the Volume Dialog.
+        val root: ViewGroup = dialog.requireViewById(R.id.volume_dialog_root)
+        // Volume Dialog container view that contains the dialog itself without the floating sliders
+        val container: View = root.requireViewById(R.id.volume_dialog_container)
+        container.alpha = 0f
+        container.repeatWhenAttached {
+            root.viewModel(
                 traceName = "VolumeDialogViewBinder",
                 minWindowLifecycleState = WindowLifecycleState.ATTACHED,
-                factory = { viewModelFactory.create() },
+                factory = { dialogViewModelFactory.create() },
             ) { viewModel ->
-                viewModel.dialogTitle.onEach { dialog.window?.setTitle(it) }.launchIn(this)
+                animateVisibility(container, dialog, viewModel.dialogVisibilityModel)
 
-                animateVisibility(view, dialog, viewModel.dialogVisibilityModel)
+                viewModel.dialogTitle.onEach { dialog.window?.setTitle(it) }.launchIn(this)
+                gravityViewModel.dialogGravity
+                    .onEach { container.setLayoutGravity(it) }
+                    .launchIn(this)
+
+                launch { root.viewTreeObserver.computeInternalInsetsListener(root) }
 
                 awaitCancellation()
             }
         }
-        volumeDialogRingerViewBinder.bind(view)
-        slidersViewBinder.bind(view)
-        settingsButtonViewBinder.bind(view)
-    }
-
-    /** Configures [Window] for the [Dialog]. */
-    private fun setupDialog(dialog: Dialog) {
-        with(dialog.window!!) {
-            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
-            addFlags(
-                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
-                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
-                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
-                    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
-            )
-            addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
-
-            requestFeature(Window.FEATURE_NO_TITLE)
-            setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
-            setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
-            setWindowAnimations(-1)
-            setFormat(PixelFormat.TRANSLUCENT)
-
-            attributes =
-                attributes.apply {
-                    title = "VolumeDialog" // Not the same as Window#setTitle
-                }
-            setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-
-            gravityViewModel.dialogGravity.onEach { setGravity(it) }.launchIn(coroutineScope)
-        }
-        dialog.setContentView(R.layout.volume_dialog)
-        dialog.setCanceledOnTouchOutside(true)
+        volumeDialogRingerViewBinder.bind(root)
+        slidersViewBinder.bind(root)
+        settingsButtonViewBinder.bind(root)
     }
 
     private fun CoroutineScope.animateVisibility(
@@ -209,4 +188,33 @@
         }
         animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
     }
+
+    private suspend fun ViewTreeObserver.computeInternalInsetsListener(viewGroup: ViewGroup) =
+        suspendCancellableCoroutine<Unit> { continuation ->
+            val listener =
+                ViewTreeObserver.OnComputeInternalInsetsListener { inoutInfo ->
+                    viewGroup.fillTouchableBounds(inoutInfo)
+                }
+            addOnComputeInternalInsetsListener(listener)
+            continuation.invokeOnCancellation { removeOnComputeInternalInsetsListener(listener) }
+        }
+
+    private fun ViewGroup.fillTouchableBounds(internalInsetsInfo: InternalInsetsInfo) {
+        for (child in children) {
+            val boundsRect = Rect()
+            internalInsetsInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
+
+            child.getBoundsInWindow(boundsRect, false)
+            internalInsetsInfo.touchableRegion.op(boundsRect, Region.Op.UNION)
+        }
+        val boundsRect = Rect()
+        getBoundsInWindow(boundsRect, false)
+    }
+
+    private fun View.setLayoutGravity(@GravityInt newGravity: Int) {
+        val frameLayoutParams =
+            layoutParams as? FrameLayout.LayoutParams
+                ?: error("View must be a child of a FrameLayout")
+        layoutParams = frameLayoutParams.apply { gravity = newGravity }
+    }
 }
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 8022e6e..f52f039 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
@@ -22,7 +22,9 @@
 import android.hardware.input.fakeInputManager
 import android.view.windowManager
 import com.android.systemui.broadcast.broadcastDispatcher
-import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils
 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
@@ -40,6 +42,7 @@
 import com.android.systemui.keyguard.data.repository.fakeCommandQueue
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.model.sysUiState
@@ -73,10 +76,18 @@
 var Kosmos.shortcutHelperCurrentAppShortcutsSource: KeyboardShortcutGroupsSource by
     Kosmos.Fixture { CurrentAppShortcutsSource(windowManager) }
 
-val Kosmos.shortcutHelperCategoriesRepository by
+val Kosmos.shortcutCategoriesUtils by
     Kosmos.Fixture {
-        ShortcutHelperCategoriesRepository(
+        ShortcutCategoriesUtils(
             applicationContext,
+            backgroundCoroutineContext,
+            fakeInputManager.inputManager,
+        )
+    }
+
+val Kosmos.defaultShortcutCategoriesRepository by
+    Kosmos.Fixture {
+        DefaultShortcutCategoriesRepository(
             applicationCoroutineScope,
             testDispatcher,
             shortcutHelperSystemShortcutsSource,
@@ -86,6 +97,18 @@
             shortcutHelperCurrentAppShortcutsSource,
             fakeInputManager.inputManager,
             shortcutHelperStateRepository,
+            shortcutCategoriesUtils,
+        )
+    }
+
+val Kosmos.customShortcutCategoriesRepository by
+    Kosmos.Fixture {
+        CustomShortcutCategoriesRepository(
+            shortcutHelperStateRepository,
+            userTracker,
+            applicationCoroutineScope,
+            testDispatcher,
+            shortcutCategoriesUtils,
         )
     }
 
@@ -112,7 +135,7 @@
     }
 
 val Kosmos.shortcutHelperCategoriesInteractor by
-    Kosmos.Fixture { ShortcutHelperCategoriesInteractor(shortcutHelperCategoriesRepository) }
+    Kosmos.Fixture { ShortcutHelperCategoriesInteractor(defaultShortcutCategoriesRepository) }
 
 val Kosmos.shortcutHelperViewModel by
     Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt
new file mode 100644
index 0000000..92eeef9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/data/repository/SystemStatusEventAnimationRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.statusbar.events.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
+import kotlinx.coroutines.flow.MutableStateFlow
+
+val Kosmos.systemStatusEventAnimationRepository: FakeSystemStatusEventAnimationRepository by
+    Kosmos.Fixture { FakeSystemStatusEventAnimationRepository() }
+
+class FakeSystemStatusEventAnimationRepository : SystemStatusEventAnimationRepository {
+    override val animationState = MutableStateFlow(SystemEventAnimationState.Idle)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt
new file mode 100644
index 0000000..7513fea
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/events/domain/interactor/SystemStatusEventAnimationInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.statusbar.events.domain.interactor
+
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
+
+val Kosmos.systemStatusEventAnimationInteractor by
+    Kosmos.Fixture {
+        SystemStatusEventAnimationInteractor(
+            repo = systemStatusEventAnimationRepository,
+            configurationInteractor = configurationInteractor,
+            scope = applicationCoroutineScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
index 3a7ada2..03e4c89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
+import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.phone.domain.interactor.lightsOutInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.interactor.collapsedStatusBarInteractor
@@ -40,6 +41,7 @@
             sceneContainerOcclusionInteractor,
             shadeInteractor,
             ongoingActivityChipsViewModel,
+            systemStatusEventAnimationInteractor,
             applicationCoroutineScope,
         )
     }
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
index 3726ca9..b389a67 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh
@@ -30,7 +30,7 @@
 EOF
 }
 
-source "${0%/*}"/../../common.sh
+source "${0%/*}"/../common.sh
 
 SCRIPT_NAME="${0##*/}"
 
@@ -61,7 +61,6 @@
 done
 shift $(($OPTIND - 1))
 
-
 # Build the dump files, which are the input of this test.
 run m  dump-jar tiny-framework-dump-test
 
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index e8fa417..afdc0c0 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -119,9 +119,16 @@
                 == BiometricManager.Authenticators.IDENTITY_CHECK;
         boolean isMandatoryBiometricsAuthentication = false;
 
+        final int effectiveUserId;
+        if (Flags.effectiveUserBp()) {
+            effectiveUserId = userManager.getCredentialOwnerProfile(userId);
+        } else {
+            effectiveUserId = userId;
+        }
+
         if (dropCredentialFallback(promptInfo.getAuthenticators(),
                 settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
-                        userId), trustManager)) {
+                        effectiveUserId), trustManager)) {
             isMandatoryBiometricsAuthentication = true;
             promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
             if (promptInfo.getNegativeButtonText() == null) {
@@ -145,13 +152,6 @@
         final List<BiometricSensor> eligibleSensors = new ArrayList<>();
         final List<Pair<BiometricSensor, Integer>> ineligibleSensors = new ArrayList<>();
 
-        final int effectiveUserId;
-        if (Flags.effectiveUserBp()) {
-            effectiveUserId = userManager.getCredentialOwnerProfile(userId);
-        } else {
-            effectiveUserId = userId;
-        }
-
         if (biometricRequested) {
             for (BiometricSensor sensor : sensors) {
 
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 97f4a5c..eeac260 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -32,6 +32,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
@@ -75,6 +76,7 @@
     private final ChangeReporter mChangeReporter;
     private final CompatConfig mCompatConfig;
     private final AndroidBuildClassifier mBuildClassifier;
+    private Boolean mIsWear;
 
     public PlatformCompat(Context context) {
         super(PermissionEnforcer.fromContext(context));
@@ -511,9 +513,16 @@
     }
 
     private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) {
+
+        // mIsWear doesn't need to be locked, ok if executes twice
+        if (mIsWear == null) {
+            mIsWear = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+        }
+
         // b/282922910 - we don't want apps sharing system uid and targeting
         // older target sdk to impact all system uid apps
-        if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) {
+        if (Flags.systemUidTargetSystemSdk() && !mIsWear &&
+                uid == Process.SYSTEM_UID) {
             appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
         }
         return appInfo;
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 0124e25..bb0b190 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -21,8 +21,6 @@
 import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
 
 import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 
 import android.annotation.BinderThread;
 import android.annotation.MainThread;
@@ -51,7 +49,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.IndentingPrintWriter;
@@ -215,7 +212,7 @@
     }
 
     private void initKeyCombinationRules() {
-        if (!useKeyGestureEventHandler() || !useKeyGestureEventHandlerMultiPressGestures()) {
+        if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
             return;
         }
         // TODO(b/358569822): Handle Power, Back key properly since key combination gesture is
@@ -441,7 +438,8 @@
 
     public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
         final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
-        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+        if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
+                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
             return mKeyCombinationManager.interceptKey(event, interactive);
         }
         return false;
@@ -457,15 +455,18 @@
         final long keyConsumed = -1;
         final long keyNotConsumed = 0;
 
-        if (mKeyCombinationManager.isKeyConsumed(event)) {
-            return keyConsumed;
-        }
+        if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
+            if (mKeyCombinationManager.isKeyConsumed(event)) {
+                return keyConsumed;
+            }
 
-        if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
-            final long now = SystemClock.uptimeMillis();
-            final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
-            if (now < interceptTimeout) {
-                return interceptTimeout - now;
+            if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+                final long now = SystemClock.uptimeMillis();
+                final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(
+                        keyCode);
+                if (now < interceptTimeout) {
+                    return interceptTimeout - now;
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/integrity/model/BitInputStream.java b/services/core/java/com/android/server/integrity/model/BitInputStream.java
deleted file mode 100644
index e7cc81e..0000000
--- a/services/core/java/com/android/server/integrity/model/BitInputStream.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.integrity.model;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/** A wrapper class for reading a stream of bits.
- *
- * <p>Note: this class reads from underlying stream byte-by-byte. It is advised to apply buffering
- * to underlying streams.
- */
-public class BitInputStream {
-
-    private long mBitsRead;
-
-    private InputStream mInputStream;
-
-    private byte mCurrentByte;
-
-    public BitInputStream(InputStream inputStream) {
-        mInputStream = inputStream;
-    }
-
-    /**
-     * Read the next number of bits from the stream.
-     *
-     * @param numOfBits The number of bits to read.
-     * @return The value read from the stream.
-     */
-    public int getNext(int numOfBits) throws IOException {
-        int component = 0;
-        int count = 0;
-
-        while (count++ < numOfBits) {
-            if (mBitsRead % 8 == 0) {
-                mCurrentByte = getNextByte();
-            }
-            int offset = 7 - (int) (mBitsRead % 8);
-
-            component <<= 1;
-            component |= (mCurrentByte >>> offset) & 1;
-
-            mBitsRead++;
-        }
-
-        return component;
-    }
-
-    /** Check if there are bits left in the stream. */
-    public boolean hasNext() throws IOException {
-        return mInputStream.available() > 0;
-    }
-
-    private byte getNextByte() throws IOException {
-        return (byte) mInputStream.read();
-    }
-}
diff --git a/services/core/java/com/android/server/integrity/model/BitOutputStream.java b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
deleted file mode 100644
index 14b35fd..0000000
--- a/services/core/java/com/android/server/integrity/model/BitOutputStream.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.integrity.model;
-
-import static com.android.server.integrity.model.ComponentBitSize.BYTE_BITS;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-
-/** A wrapper class for writing a stream of bits. */
-public class BitOutputStream {
-
-    private static final int BUFFER_SIZE = 4 * 1024;
-
-    private int mNextBitIndex;
-
-    private final OutputStream mOutputStream;
-    private final byte[] mBuffer;
-
-    public BitOutputStream(OutputStream outputStream) {
-        mBuffer = new byte[BUFFER_SIZE];
-        mNextBitIndex = 0;
-        mOutputStream = outputStream;
-    }
-
-    /**
-     * Set the next number of bits in the stream to value.
-     *
-     * @param numOfBits The number of bits used to represent the value.
-     * @param value The value to convert to bits.
-     */
-    public void setNext(int numOfBits, int value) throws IOException {
-        if (numOfBits <= 0) {
-            return;
-        }
-
-        // optional: we can do some clever size checking to "OR" an entire segment of bits instead
-        // of setting bits one by one, but it is probably not worth it.
-        int nextBitMask = 1 << (numOfBits - 1);
-        while (numOfBits-- > 0) {
-            setNext((value & nextBitMask) != 0);
-            nextBitMask >>>= 1;
-        }
-    }
-
-    /**
-     * Set the next bit in the stream to value.
-     *
-     * @param value The value to set the bit to
-     */
-    public void setNext(boolean value) throws IOException {
-        int byteToWrite = mNextBitIndex / BYTE_BITS;
-        if (byteToWrite == BUFFER_SIZE) {
-            mOutputStream.write(mBuffer);
-            reset();
-            byteToWrite = 0;
-        }
-        if (value) {
-            mBuffer[byteToWrite] |= 1 << (BYTE_BITS - 1 - (mNextBitIndex % BYTE_BITS));
-        }
-        mNextBitIndex++;
-    }
-
-    /** Set the next bit in the stream to true. */
-    public void setNext() throws IOException {
-        setNext(/* value= */ true);
-    }
-
-    /**
-     * Flush the data written to the underlying {@link java.io.OutputStream}. Any unfinished bytes
-     * will be padded with 0.
-     */
-    public void flush() throws IOException {
-        int endByte = mNextBitIndex / BYTE_BITS;
-        if (mNextBitIndex % BYTE_BITS != 0) {
-            // If next bit is not the first bit of a byte, then mNextBitIndex / BYTE_BITS would be
-            // the byte that includes already written bits. We need to increment it so this byte
-            // gets written.
-            endByte++;
-        }
-        mOutputStream.write(mBuffer, 0, endByte);
-        reset();
-    }
-
-    /** Reset this output stream to start state. */
-    private void reset() {
-        mNextBitIndex = 0;
-        Arrays.fill(mBuffer, (byte) 0);
-    }
-}
diff --git a/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java b/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java
deleted file mode 100644
index ceed054..0000000
--- a/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java
+++ /dev/null
@@ -1,63 +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.server.integrity.model;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * An output stream that tracks the total number written bytes since construction and allows
- * querying this value any time during the execution.
- *
- * <p>This class is used for constructing the rule indexing.
- */
-public class ByteTrackedOutputStream extends OutputStream {
-
-    private int mWrittenBytesCount;
-    private final OutputStream mOutputStream;
-
-    public ByteTrackedOutputStream(OutputStream outputStream) {
-        mWrittenBytesCount = 0;
-        mOutputStream = outputStream;
-    }
-
-    @Override
-    public void write(int b) throws IOException {
-        mWrittenBytesCount++;
-        mOutputStream.write(b);
-    }
-
-    /**
-     * Writes the given bytes into the output stream provided in constructor and updates the total
-     * number of written bytes.
-     */
-    @Override
-    public void write(byte[] bytes) throws IOException {
-        write(bytes, 0, bytes.length);
-    }
-
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException {
-        mWrittenBytesCount += len;
-        mOutputStream.write(b, off, len);
-    }
-
-    /** Returns the total number of bytes written into the output stream at the requested time. */
-    public int getWrittenBytesCount() {
-        return mWrittenBytesCount;
-    }
-}
diff --git a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
deleted file mode 100644
index 94e6708..0000000
--- a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.integrity.model;
-
-import android.content.integrity.Rule;
-
-/**
- * A helper class containing information about the binary representation of different {@link Rule}
- * components.
- */
-public final class ComponentBitSize {
-    public static final int FORMAT_VERSION_BITS = 8;
-
-    public static final int EFFECT_BITS = 3;
-    public static final int KEY_BITS = 4;
-    public static final int OPERATOR_BITS = 3;
-    public static final int CONNECTOR_BITS = 2;
-    public static final int SEPARATOR_BITS = 3;
-    public static final int VALUE_SIZE_BITS = 8;
-    public static final int IS_HASHED_BITS = 1;
-
-    public static final int ATOMIC_FORMULA_START = 0;
-    public static final int COMPOUND_FORMULA_START = 1;
-    public static final int COMPOUND_FORMULA_END = 2;
-    public static final int INSTALLER_ALLOWED_BY_MANIFEST_START = 3;
-
-    public static final int DEFAULT_FORMAT_VERSION = 1;
-    public static final int SIGNAL_BIT = 1;
-
-    public static final int BYTE_BITS = 8;
-}
diff --git a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java b/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
deleted file mode 100644
index 0c4052a..0000000
--- a/services/core/java/com/android/server/integrity/model/IndexingFileConstants.java
+++ /dev/null
@@ -1,27 +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.server.integrity.model;
-
-/**  A helper class containing special indexing file constants. */
-public final class IndexingFileConstants {
-    // We empirically experimented with different block sizes and identified that 50 is in the
-    // optimal range of efficient computation.
-    public static final int INDEXING_BLOCK_SIZE = 50;
-
-    public static final String START_INDEXING_KEY = "START_KEY";
-    public static final String END_INDEXING_KEY = "END_KEY";
-}
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index acf62dc..7469c92 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -64,6 +64,7 @@
 import android.util.Xml;
 
 import com.android.internal.R;
+import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
@@ -1632,6 +1633,14 @@
                     continue;
                 }
 
+                // If the trunkstable feature flag is disabled for this
+                // exception, skip the tag.
+                if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(
+                        /* pkg= */ null, parser, /* allowNoNamespace= */ true)) {
+                    XmlUtils.skipCurrentTag(parser);
+                    continue;
+                }
+
                 final boolean fixed =
                         parser.getAttributeBoolean(null, ATTR_FIXED, false);
                 final boolean whitelisted =
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8d039f1..dda5bcf 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -88,7 +88,6 @@
 import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
 import static com.android.hardware.input.Flags.modifierShortcutDump;
 import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
-import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
 import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
 import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
@@ -2496,7 +2495,7 @@
 
     private void initKeyCombinationRules() {
         mKeyCombinationManager = new KeyCombinationManager(mHandler);
-        if (useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures()) {
+        if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
             return;
         }
         final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
@@ -3442,7 +3441,7 @@
                             + keyguardOn() + " canceled=" + event.isCanceled());
         }
 
-        if (!useKeyGestureEventHandler()) {
+        if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()) {
             if (mKeyCombinationManager.isKeyConsumed(event)) {
                 return keyConsumed;
             }
@@ -5720,7 +5719,8 @@
     }
 
     private void handleKeyGesture(KeyEvent event, boolean interactive, boolean defaultDisplayOn) {
-        if (mKeyCombinationManager.interceptKey(event, interactive)) {
+        if (!InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
+                && mKeyCombinationManager.interceptKey(event, interactive)) {
             // handled by combo keys manager.
             mSingleKeyGestureDetector.reset();
             return;
diff --git a/services/core/java/com/android/server/stats/pull/psi/OWNERS b/services/core/java/com/android/server/stats/pull/psi/OWNERS
new file mode 100644
index 0000000..f72fd7c
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/OWNERS
@@ -0,0 +1,9 @@
+jackrichardson@google.com
+dbrotikovskaya@google.com
+ivokay@google.com
+gagapov@google.com
+yigitfiliz@google.com
+rswang@google.com
+evaleriano@google.com
+igorstepanov@google.com
+iyou@google.com
diff --git a/services/core/java/com/android/server/stats/pull/psi/PsiData.java b/services/core/java/com/android/server/stats/pull/psi/PsiData.java
new file mode 100644
index 0000000..d1cbf74
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/PsiData.java
@@ -0,0 +1,106 @@
+/*
+ * 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.stats.pull.psi;
+
+/**
+ * Wraps PSI (Pressure Stall Information) corresponding to a system resource. See more details about
+ * PSI, see https://docs.kernel.org/accounting/psi.html#psi-pressure-stall-information.
+ */
+public class PsiData {
+    public enum ResourceType {
+        CPU,
+        MEMORY,
+        IO
+    }
+
+    static class AppsStallInfo {
+
+        /** Past 10s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+        private final float mAvg10SecPercentage;
+
+        /** Past 60s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+        private final float mAvg60SecPercentage;
+
+        /** Past 300s average % of wasted CPU cycles when apps tasks are stalled on mResourceType.*/
+        private final float mAvg300SecPercentage;
+
+        /** Total number of microseconds that apps tasks are stalled on mResourceType.*/
+        private final long mTotalUsec;
+
+        AppsStallInfo(
+                float avg10SecPercentage, float avg60SecPercentage,
+                float avg300SecPercentage, long totalUsec) {
+            mAvg10SecPercentage = avg10SecPercentage;
+            mAvg60SecPercentage = avg60SecPercentage;
+            mAvg300SecPercentage = avg300SecPercentage;
+            mTotalUsec = totalUsec;
+        }
+    }
+
+    /** The system resource type of this {@code PsiData}. */
+    private final ResourceType mResourceType;
+
+    /** Info on some tasks are stalled on mResourceType. */
+    private final AppsStallInfo mSomeAppsStallInfo;
+
+    /**
+     * Info on all non-idle tasks are stalled on mResourceType. For the CPU ResourceType,
+     * all fields will always be 0 as it's undefined.
+     */
+    private final AppsStallInfo mFullAppsStallInfo;
+
+    PsiData(
+            ResourceType resourceType,
+            AppsStallInfo someAppsStallInfo,
+            AppsStallInfo fullAppsStallInfo) {
+        mResourceType = resourceType;
+        mSomeAppsStallInfo = someAppsStallInfo;
+        mFullAppsStallInfo = fullAppsStallInfo;
+    }
+
+    public ResourceType getResourceType() {
+        return mResourceType;
+    }
+
+    public float getSomeAvg10SecPercentage() {
+        return mSomeAppsStallInfo.mAvg10SecPercentage; }
+
+    public float getSomeAvg60SecPercentage() {
+        return mSomeAppsStallInfo.mAvg60SecPercentage; }
+
+    public float getSomeAvg300SecPercentage() {
+        return mSomeAppsStallInfo.mAvg300SecPercentage; }
+
+    public long getSomeTotalUsec() {
+        return mSomeAppsStallInfo.mTotalUsec;
+    }
+
+    public float getFullAvg10SecPercentage() {
+        return mFullAppsStallInfo.mAvg10SecPercentage;
+    }
+
+    public float getFullAvg60SecPercentage() {
+        return mFullAppsStallInfo.mAvg60SecPercentage;
+    }
+
+    public float getFullAvg300SecPercentage() {
+        return mFullAppsStallInfo.mAvg300SecPercentage; }
+
+    public long getFullTotalUsec() {
+        return mFullAppsStallInfo.mTotalUsec;
+    }
+}
diff --git a/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java b/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java
new file mode 100644
index 0000000..5d0d7e1
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/psi/PsiExtractor.java
@@ -0,0 +1,161 @@
+/*
+ * 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.stats.pull.psi;
+
+import static java.util.stream.Collectors.joining;
+
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PsiExtractor {
+    private static final String TAG = "PsiExtractor";
+
+    // Paths for PSI files are guarded by SELinux policy. PCS needs to be explicitly
+    // allowlisted to access these files.
+    private static final String PSI_MEMORY_PATH = "/proc/pressure/memory";
+    private static final String PSI_IO_PATH = "/proc/pressure/io";
+    private static final String PSI_CPU_PATH = "/proc/pressure/cpu";
+
+    // The patterns matching a line of PSI output such as
+    // "some avg10=0.12 avg60=0.34 avg300=0.56 total=123456" or
+    // "full avg10=0.12 avg60=0.34 avg300=0.56 total=123456" to extract the stalling percentage
+    // values for "some" and "full" line of PSI output respectively.
+    private static final String PSI_PATTERN_TEMPLATE =
+            ".*{0} avg10=(\\d+.\\d+) avg60=(\\d+.\\d+) avg300=(\\d+.\\d+) total=(\\d+).*";
+    private static final String SOME = "some";
+    private static final String FULL = "full";
+    private final PsiReader mPsiReader;
+
+    public PsiExtractor() {
+        mPsiReader = new PsiReader();
+    }
+    public PsiExtractor(PsiReader psiReader) {
+        mPsiReader = psiReader;
+    }
+
+    /**
+    * Parses /pressure/proc/{resourceType} kernel file to extract the Pressure Stall Information
+    * (PSI), more information: can be found at https://docs.kernel.org/accounting/psi.html.
+    *
+    * @param resourceType (Memory/CPU/IO) to get the PSI for.
+    */
+    @Nullable
+    public PsiData getPsiData(PsiData.ResourceType resourceType) {
+        String psiFileData;
+        if (resourceType == PsiData.ResourceType.MEMORY) {
+            psiFileData = mPsiReader.read(PSI_MEMORY_PATH);
+        } else if (resourceType == PsiData.ResourceType.IO) {
+            psiFileData = mPsiReader.read(PSI_IO_PATH);
+        } else if (resourceType == PsiData.ResourceType.CPU) {
+            psiFileData = mPsiReader.read(PSI_CPU_PATH);
+        } else {
+            Log.w(TAG, "PsiExtractor failure: cannot read kernel source file, returning null");
+            return null;
+        }
+        return parsePsiData(psiFileData, resourceType);
+    }
+
+    @Nullable
+    private static PsiData.AppsStallInfo parsePsiString(
+            String psiFileData, String appType, PsiData.ResourceType resourceType) {
+        // There is an extra case of file content: the CPU full is undefined and isn't reported for
+        // earlier versions. It should be always propagated as 0, but for the current logic purposes
+        // we will report atom only if at least one value (some/full) is presented. Thus, hardcoding
+        // the "full" line as 0 only when the "some" line is presented.
+        if (appType == FULL && resourceType == PsiData.ResourceType.CPU) {
+            if (psiFileData.contains(SOME) && !psiFileData.contains(FULL)) {
+                return new PsiData.AppsStallInfo((float) 0.0, (float) 0.0, (float) 0.0, 0);
+            }
+        }
+
+        Pattern psiStringPattern = Pattern.compile(
+                MessageFormat.format(PSI_PATTERN_TEMPLATE, appType));
+        Matcher psiLineMatcher = psiStringPattern.matcher(psiFileData);
+
+        // Parsing the line starts with "some" in the expected output.
+        // The line for "some" should always be present in PSI output. The output must be somehow
+        // malformed if the line cannot be matched.
+        if (!psiLineMatcher.find()) {
+            Log.w(TAG,
+                    "Returning null: the line \"" +  appType + "\" is not in expected pattern.");
+            return null;
+        }
+        try {
+            return new PsiData.AppsStallInfo(
+                    Float.parseFloat(psiLineMatcher.group(1)),
+                    Float.parseFloat(psiLineMatcher.group(2)),
+                    Float.parseFloat(psiLineMatcher.group(3)),
+                    Long.parseLong(psiLineMatcher.group(4)));
+        } catch (NumberFormatException e) {
+            Log.w(TAG,
+                    "Returning null: some value in line \"" +  appType
+                            + "\" cannot be parsed as numeric.");
+            return null;
+        }
+    }
+
+    @Nullable
+    private static PsiData parsePsiData(
+                                         String psiFileData, PsiData.ResourceType resourceType) {
+        PsiData.AppsStallInfo someAppsStallInfo = parsePsiString(psiFileData, SOME, resourceType);
+        PsiData.AppsStallInfo fullAppsStallInfo = parsePsiString(psiFileData, FULL, resourceType);
+
+        if (someAppsStallInfo == null && fullAppsStallInfo == null) {
+            Log.w(TAG, "Returning empty PSI: some or full line are failed to parse");
+            return null;
+        } else if (someAppsStallInfo == null) {
+            Log.d(TAG, "Replacing some info with empty PSI record for the resource type "
+                    + resourceType);
+            someAppsStallInfo = new PsiData.AppsStallInfo(
+                    (float) -1.0, (float) -1.0, (float) -1.0, -1);
+        } else if (fullAppsStallInfo == null) {
+            Log.d(TAG, "Replacing full info with empty PSI record for the resource type "
+                    + resourceType);
+            fullAppsStallInfo = new PsiData.AppsStallInfo(
+                    (float) -1.0, (float) -1.0, (float) -1.0, -1);
+        }
+        return new PsiData(resourceType, someAppsStallInfo, fullAppsStallInfo);
+    }
+
+    /** Dependency class */
+    public static class PsiReader {
+        /**
+        * Reads file from provided path and returns its content if the file found, null otherwise.
+        *
+        * @param filePath file path to read.
+        */
+        @Nullable
+        public String read(String filePath) {
+            try (BufferedReader br =
+                         new BufferedReader(new InputStreamReader(
+                                 new FileInputStream(filePath)))) {
+                return br.lines().collect(joining(System.lineSeparator()));
+            } catch (IOException e) {
+                Log.w(TAG, "Cannot read file " +  filePath);
+                return null;
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b27a2a5..1c11c67 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8896,6 +8896,7 @@
                 mAppCompatController.getAppCompatSizeCompatModePolicy();
 
         if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()
+                && mAppCompatDisplayInsets != null
                 && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
             // App prefers to keep its original size.
             // If the size compat is from previous fixed orientation letterboxing, we may want to
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index fa2c716..e8eae4f 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -70,15 +70,8 @@
         mAppCompatAspectRatioState.reset();
     }
 
-    float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
+    private float getDesiredAspectRatio(@NonNull Configuration newParentConfig,
             @NonNull Rect parentBounds) {
-        // If in camera compat mode, aspect ratio from the camera compat policy has priority over
-        // default letterbox aspect ratio.
-        if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(
-                mActivityRecord)) {
-            return AppCompatCameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
-        }
-
         final float letterboxAspectRatioOverride =
                 mAppCompatOverrides.getAppCompatAspectRatioOverrides()
                         .getFixedOrientationLetterboxAspectRatio(newParentConfig);
@@ -120,7 +113,16 @@
         if (mTransparentPolicy.isRunning()) {
             return mTransparentPolicy.getInheritedMinAspectRatio();
         }
+
         final ActivityInfo info = mActivityRecord.info;
+
+        // If in camera compat mode, aspect ratio from the camera compat policy has priority over
+        // the default aspect ratio.
+        if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(mActivityRecord)) {
+            return Math.max(AppCompatCameraPolicy.getCameraCompatMinAspectRatio(mActivityRecord),
+                    info.getMinAspectRatio());
+        }
+
         final AppCompatAspectRatioOverrides aspectRatioOverrides =
                 mAppCompatOverrides.getAppCompatAspectRatioOverrides();
         if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 8c5689c1..8be66cc 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -234,7 +234,7 @@
     }
 
     // TODO(b/369070416): have policies implement the same interface.
-    static float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+    static float getCameraCompatMinAspectRatio(@NonNull ActivityRecord activity) {
         final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
         if (cameraPolicy == null) {
             return 1.0f;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 2664d8c..6091b83 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -771,20 +771,21 @@
                 mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
             }
 
-            boolean hasChange = false;
-            if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
+            final boolean startedGoingAway = (!lastKeyguardGoingAway && mKeyguardGoingAway);
+            final boolean occludedChanged = (lastOccluded != mOccluded);
+
+            if (startedGoingAway) {
                 writeEventLog("dismissIfInsecure");
                 controller.handleDismissInsecureKeyguard(display);
                 controller.scheduleGoingAwayTimeout(mDisplayId);
-                hasChange = true;
-            } else if (lastOccluded != mOccluded) {
+            }
+            if (occludedChanged && (reduceKeyguardTransitions() || !startedGoingAway)) {
                 controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
-                hasChange = true;
             }
 
             // Collect the participants for shell transition, so that transition won't happen too
             // early since the transition was set ready.
-            if (hasChange && top != null && (mOccluded || mKeyguardGoingAway)) {
+            if (top != null && (startedGoingAway || (occludedChanged && mOccluded))) {
                 display.mTransitionController.collect(top);
             }
         }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c653038..50f3f39 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -21030,6 +21030,27 @@
     }
 
     @Override
+    public boolean removeManagedProfile(int userId) {
+        Preconditions.checkCallAuthorization(
+                hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS));
+
+        if (!isManagedProfile(userId)){
+            throw new IllegalArgumentException("Cannot remove user as it is not a managed profile");
+        }
+
+        boolean success = false;
+        final long identity = Binder.clearCallingIdentity();
+        try{
+            success = mUserManager.removeUserEvenWhenDisallowed(userId);
+        } catch (Exception e) {
+            Slogf.e(LOG_TAG, "Remove managed profile failed due to: ", e);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+        return success;
+    }
+
+    @Override
     public UserHandle createAndProvisionManagedProfile(
             @NonNull ManagedProfileProvisioningParams provisioningParams,
             @NonNull String callerPackage) {
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
index fead05b..5df9dd5 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -35,6 +35,14 @@
     public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
 
     /**
+     * Set whether supervision is enabled for the specified user.
+     *
+     * @param userId The user to set the supervision state for
+     * @param enabled Whether or not the user should be supervised
+     */
+    public abstract void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled);
+
+    /**
      * Sets whether the supervision lock screen should be shown for the specified user
      *
      * @param userId The user set the superivision state for
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 4c515c1..67e2547 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -19,7 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManagerInternal;
 import android.app.supervision.ISupervisionManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Bundle;
@@ -28,19 +30,19 @@
 import android.os.ShellCallback;
 import android.util.SparseArray;
 
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
 import com.android.server.pm.UserManagerInternal;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
-/**
- * Service for handling system supervision.
- */
+/** Service for handling system supervision. */
 public class SupervisionService extends ISupervisionManager.Stub {
     private static final String LOG_TAG = "SupervisionService";
 
@@ -52,14 +54,25 @@
     @GuardedBy("getLockObject()")
     private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
 
+    private final DevicePolicyManagerInternal mDpmInternal;
     private final UserManagerInternal mUserManagerInternal;
 
     public SupervisionService(Context context) {
         mContext = context.createAttributionContext(LOG_TAG);
+        mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class);
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
     }
 
+    void syncStateWithDevicePolicyManager(TargetUser user) {
+        if (user.isPreCreated()) return;
+
+        // Ensure that supervision is enabled when supervision app is the profile owner.
+        if (android.app.admin.flags.Flags.enableSupervisionServiceSync() && isProfileOwner(user)) {
+            setSupervisionEnabledForUser(user.getUserIdentifier(), true);
+        }
+    }
+
     @Override
     public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
         synchronized (getLockObject()) {
@@ -74,14 +87,15 @@
             @Nullable FileDescriptor err,
             @NonNull String[] args,
             @Nullable ShellCallback callback,
-            @NonNull ResultReceiver resultReceiver) throws RemoteException {
+            @NonNull ResultReceiver resultReceiver)
+            throws RemoteException {
         new SupervisionServiceShellCommand(this)
                 .exec(this, in, out, err, args, callback, resultReceiver);
     }
 
     @Override
-    protected void dump(@NonNull FileDescriptor fd,
-            @NonNull PrintWriter printWriter, @Nullable String[] args) {
+    protected void dump(
+            @NonNull FileDescriptor fd, @NonNull PrintWriter printWriter, @Nullable String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
 
         try (var pw = new IndentingPrintWriter(printWriter, "  ")) {
@@ -120,6 +134,17 @@
         }
     }
 
+    /** Returns whether the supervision app has profile owner status. */
+    private boolean isProfileOwner(TargetUser user) {
+        ComponentName profileOwner = mDpmInternal.getProfileOwnerAsUser(user.getUserIdentifier());
+        if (profileOwner == null) {
+            return false;
+        }
+
+        String configPackage = mContext.getResources().getString(R.string.config_systemSupervision);
+        return profileOwner.getPackageName().equals(configPackage);
+    }
+
     public static class Lifecycle extends SystemService {
         private final SupervisionService mSupervisionService;
 
@@ -133,13 +158,24 @@
             publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
             publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
         }
+
+        @Override
+        public void onUserStarting(@NonNull TargetUser user) {
+            mSupervisionService.syncStateWithDevicePolicyManager(user);
+        }
     }
 
-    final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+    final SupervisionManagerInternal mInternal = new SupervisionManagerInternalImpl();
+
+    private final class SupervisionManagerInternalImpl extends SupervisionManagerInternal {
+        @Override
         public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
-            synchronized (getLockObject()) {
-                return getUserDataLocked(userId).supervisionEnabled;
-            }
+            return SupervisionService.this.isSupervisionEnabledForUser(userId);
+        }
+
+        @Override
+        public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+            SupervisionService.this.setSupervisionEnabledForUser(userId, enabled);
         }
 
         @Override
@@ -151,7 +187,7 @@
                 data.supervisionLockScreenOptions = options;
             }
         }
-    };
+    }
 
     private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index c418151..96c6cbc 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -123,7 +123,7 @@
             Assert.assertEquals("mic mute reporting wrong value",
                     muted, mAudioService.isMicrophoneMuted());
             // verify the intent for mic mute changed is supposed to be fired
-            Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+            mTestLooper.dispatchAll();
             verify(mSpySystemServer, times(1))
                     .sendMicrophoneMuteChangedIntent();
             reset(mSpySystemServer);
@@ -148,7 +148,7 @@
             Assert.assertEquals("mic mute reporting wrong value",
                     !muted, mAudioService.isMicrophoneMuted());
             // verify the intent for mic mute changed is supposed to be fired
-            Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+            mTestLooper.dispatchAll();
             verify(mSpySystemServer, times(1))
                     .sendMicrophoneMuteChangedIntent();
             reset(mSpySystemServer);
@@ -159,8 +159,7 @@
     public void testRingNotifAlias() throws Exception {
         Log.i(TAG, "running testRingNotifAlias");
         Assert.assertNotNull(mAudioService);
-        // TODO add initialization message that can be caught here instead of sleeping
-        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); // wait for full AudioService initialization
+        mTestLooper.dispatchAll(); // wait for full AudioService initialization
 
         // test with aliasing RING and NOTIFICATION
         mAudioService.setNotifAliasRingForTest(true);
@@ -171,7 +170,7 @@
         mAudioService.setStreamVolume(AudioSystem.STREAM_NOTIFICATION,
                 ringVol, 0, "bla");
         mAudioService.setStreamVolume(AudioSystem.STREAM_RING, ringMaxVol, 0, "bla");
-        Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+        mTestLooper.dispatchAll();
         Assert.assertEquals(ringMaxVol,
                 mAudioService.getStreamVolume(AudioSystem.STREAM_NOTIFICATION));
 
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index cf628ad..b81bf3c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -332,6 +332,28 @@
         assertThat(preAuthInfo.callingUserId).isEqualTo(USER_ID);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EFFECTIVE_USER_BP)
+    public void testCredentialOwnerIdAsUserId_forMandatoryBiometrics() throws Exception {
+        when(mUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(OWNER_ID);
+        when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+                OWNER_ID)).thenReturn(true);
+        when(mSettingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+                USER_ID)).thenReturn(false);
+        when(mTrustManager.isInSignificantPlace()).thenReturn(false);
+
+        final BiometricSensor sensor = getFaceSensor();
+        final PromptInfo promptInfo = new PromptInfo();
+        promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
+        promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
+        final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
+                mSettingObserver, List.of(sensor), USER_ID , promptInfo, TEST_PACKAGE_NAME,
+                false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager,
+                mUserManager);
+
+        assertThat(preAuthInfo.getIsMandatoryBiometricsAuthentication()).isTrue();
+    }
+
     private BiometricSensor getFingerprintSensor() {
         BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FINGERPRINT,
                 TYPE_FINGERPRINT, BiometricManager.Authenticators.BIOMETRIC_STRONG,
diff --git a/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java b/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java
deleted file mode 100644
index 57274bf..0000000
--- a/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java
+++ /dev/null
@@ -1,70 +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.server.integrity.model;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayOutputStream;
-
-@RunWith(JUnit4.class)
-public class ByteTrackedOutputStreamTest {
-
-    @Test
-    public void testConstructorStartsWithZeroBytesWritten() {
-        ByteTrackedOutputStream byteTrackedOutputStream =
-                new ByteTrackedOutputStream(new ByteArrayOutputStream());
-
-        assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(0);
-    }
-
-    @Test
-    public void testSuccessfulWriteAndValidateWrittenBytesCount_directFromByteArray()
-            throws Exception {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        ByteTrackedOutputStream byteTrackedOutputStream = new ByteTrackedOutputStream(outputStream);
-
-        byte[] outputContent = "This is going to be outputed for tests.".getBytes();
-        byteTrackedOutputStream.write(outputContent);
-
-        assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(outputContent.length);
-        assertThat(outputStream.toByteArray().length).isEqualTo(outputContent.length);
-    }
-
-    @Test
-    public void testSuccessfulWriteAndValidateWrittenBytesCount_fromBitStream() throws Exception {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        ByteTrackedOutputStream byteTrackedOutputStream = new ByteTrackedOutputStream(outputStream);
-
-        BitOutputStream bitOutputStream = new BitOutputStream(byteTrackedOutputStream);
-        bitOutputStream.setNext(/* numOfBits= */5, /* value= */1);
-        bitOutputStream.flush();
-
-        // Even though we wrote 5 bits, this will complete to 1 byte.
-        assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(1);
-
-        // Add a bit less than 2 bytes (10 bits).
-        bitOutputStream.setNext(/* numOfBits= */10, /* value= */1);
-        bitOutputStream.flush();
-        assertThat(byteTrackedOutputStream.getWrittenBytesCount()).isEqualTo(3);
-
-        assertThat(outputStream.toByteArray().length).isEqualTo(3);
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS b/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS
new file mode 100644
index 0000000..e068a84
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/stats/pull/psi/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/stats/pull/psi/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java b/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java
new file mode 100644
index 0000000..b563c08
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/stats/pull/psi/PsiExtractorTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.stats.pull.psi;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+
+public class PsiExtractorTest {
+    @Mock
+    private PsiExtractor.PsiReader mPsiReader;
+    private PsiExtractor mPsiExtractor;
+    // PSI file content with both some and full lines.
+    private static final String PSI_FILE_CONTENT_BOTH_LINES =
+            "some avg10=0.12 avg60=0.34 avg300=0.56 total=12345678\n"
+                    + "full avg10=0.21 avg60=0.43 avg300=0.65 total=87654321";
+    // PSI file content with only some line.
+    private static final String PSI_FILE_CONTENT_ONLY_SOME_LINE =
+            "some avg10=0.12 avg60=0.34 avg300=0.56 total=12345678";
+
+    // PSI file content with only full line.
+    private static final String PSI_FILE_CONTENT_ONLY_FULL_LINE =
+            "\nfull avg10=0.21 avg60=0.43 avg300=0.65 total=87654321";
+
+    // PSI file content that is malformed with "avg60" missing from the both lines.
+    private static final String BOTH_AVG60_MISSING_PSI_FILE_CONTENT =
+            "some avg10=0.12 avg300=0.56 total=12345678\n"
+                    + "full avg10=0.21 avg300=0.65 total=87654321";
+
+    // PSI file content that is malformed with non number "avg10" from the both lines.
+    private static final String NON_NUM_AVG10_PSI_FILE_CONTENT =
+            "some avg10=1.a2 avg300=0.56 total=12345678\n"
+                    + "full avg10=0.2s1 avg60=0.43 avg300=0.65 total=87654321";
+
+    // PSI file content that is malformed with non number "avg300" from the both lines.
+    private static final String NON_NUM_AVG300_PSI_FILE_CONTENT =
+            "some avg10=0.2 avg60=0.43 avg300=0.5ss6 total=12345678\n"
+                    + "full avg10=0.21 avg60=0.43 avg300=0.6b5 total=87654321";
+
+    // PSI file content that is malformed with non number "total"  from the both lines.
+    private static final String BOTH_TOTAL_MISSING_PSI_FILE_CONTENT =
+            "some avg10=0.2 avg60=0.43 avg300=0.56\n"
+                    + "full avg10=0.21 avg60=0.43 avg300=0.65";
+
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mPsiExtractor = new PsiExtractor(mPsiReader);
+    }
+
+    @Test
+    public void getPsiData_bothLinesPresentedAndValidMemory() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                PSI_FILE_CONTENT_BOTH_LINES);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_bothLinesPresentedAndValidCpu() {
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                PSI_FILE_CONTENT_BOTH_LINES);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_bothLinesPresentedAndValidIO() {
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                PSI_FILE_CONTENT_BOTH_LINES);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_onlySomePresentedAndValidMemory() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_SOME_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullTotalUsec(), -1);
+    }
+
+    @Test
+    public void getPsiData_onlySomePresentedAndValidCpu() {
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_SOME_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.0);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.0);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.0);
+        assertEquals(psiData.getFullTotalUsec(), 0);
+    }
+
+    @Test
+    public void getPsiData_onlySomePresentedAndValidIO() {
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_SOME_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) 0.12);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) 0.34);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) 0.56);
+        assertEquals(psiData.getSomeTotalUsec(), 12345678);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getFullTotalUsec(), -1);
+    }
+
+    @Test
+    public void getPsiData_onlyFullPresentedAndValidMemory() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_FULL_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.MEMORY);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeTotalUsec(), -1);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_onlyFullPresentedAndValidCpu() {
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_FULL_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.CPU);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeTotalUsec(), -1);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_onlyFullPresentedAndValidIO() {
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                PSI_FILE_CONTENT_ONLY_FULL_LINE);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData.getResourceType(), PsiData.ResourceType.IO);
+        assertEquals(psiData.getSomeAvg10SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg60SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeAvg300SecPercentage(), (float) -1.0);
+        assertEquals(psiData.getSomeTotalUsec(), -1);
+        assertEquals(psiData.getFullAvg10SecPercentage(), (float) 0.21);
+        assertEquals(psiData.getFullAvg60SecPercentage(), (float) 0.43);
+        assertEquals(psiData.getFullAvg300SecPercentage(), (float) 0.65);
+        assertEquals(psiData.getFullTotalUsec(), 87654321);
+    }
+
+    @Test
+    public void getPsiData_emptyFile() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn("");
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn("");
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn("");
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData, null);
+    }
+
+    @Test
+    public void getPsiData_avg60Missing() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                BOTH_AVG60_MISSING_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData, null);
+    }
+
+    @Test
+    public void getPsiData_totalMissing() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                BOTH_TOTAL_MISSING_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData, null);
+    }
+
+    @Test
+    public void getPsiData_avg10NonNum() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                NON_NUM_AVG10_PSI_FILE_CONTENT);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                NON_NUM_AVG10_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                NON_NUM_AVG10_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData, null);
+    }
+
+    @Test
+    public void getPsiData_avg300NonNum() {
+        Mockito.when(mPsiReader.read("/proc/pressure/memory")).thenReturn(
+                NON_NUM_AVG300_PSI_FILE_CONTENT);
+        PsiData psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.MEMORY);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/cpu")).thenReturn(
+                NON_NUM_AVG300_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.CPU);
+        assertEquals(psiData, null);
+
+        Mockito.when(mPsiReader.read("/proc/pressure/io")).thenReturn(
+                NON_NUM_AVG300_PSI_FILE_CONTENT);
+        psiData = mPsiExtractor.getPsiData(PsiData.ResourceType.IO);
+        assertEquals(psiData, null);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index 6bd4279..79b06236 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -16,12 +16,20 @@
 
 package com.android.server.supervision
 
+import android.app.admin.DevicePolicyManagerInternal
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.UserInfo
 import android.os.Bundle
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.R
 import com.android.server.LocalServices
+import com.android.server.SystemService.TargetUser
 import com.android.server.pm.UserManagerInternal
 import com.google.common.truth.Truth.assertThat
-import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -29,11 +37,12 @@
 import org.mockito.Mock
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.whenever
 
 /**
- * Unit tests for {@link SupervisionService}.
- * <p/>
- * Run with <code>atest SupervisionServiceTest</code>.
+ * Unit tests for [SupervisionService].
+ *
+ * Run with `atest SupervisionServiceTest`.
  */
 @RunWith(AndroidJUnit4::class)
 class SupervisionServiceTest {
@@ -41,18 +50,21 @@
         const val USER_ID = 100
     }
 
+    @get:Rule val mocks: MockitoRule = MockitoJUnit.rule()
+    @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+    @Mock private lateinit var mockDpmInternal: DevicePolicyManagerInternal
+    @Mock private lateinit var mockUserManagerInternal: UserManagerInternal
+
+    private lateinit var context: Context
     private lateinit var service: SupervisionService
 
-    @Rule
-    @JvmField
-    val mocks: MockitoRule = MockitoJUnit.rule()
-
-    @Mock
-    private lateinit var mockUserManagerInternal: UserManagerInternal
-
     @Before
-    fun setup() {
-        val context = InstrumentationRegistry.getInstrumentation().context
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+
+        LocalServices.removeServiceForTest(DevicePolicyManagerInternal::class.java)
+        LocalServices.addService(DevicePolicyManagerInternal::class.java, mockDpmInternal)
 
         LocalServices.removeServiceForTest(UserManagerInternal::class.java)
         LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
@@ -61,7 +73,46 @@
     }
 
     @Test
-    fun testSetSupervisionEnabledForUser() {
+    @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+    fun syncStateWithDevicePolicyManager_supervisionAppIsProfileOwner_enablesSupervision() {
+        val supervisionPackageName =
+            context.getResources().getString(R.string.config_systemSupervision)
+
+        whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+            .thenReturn(ComponentName(supervisionPackageName, "MainActivity"))
+
+        service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID))
+
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+    fun syncStateWithDevicePolicyManager_userPreCreated_doesNotEnableSupervision() {
+        val supervisionPackageName =
+            context.getResources().getString(R.string.config_systemSupervision)
+
+        whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+            .thenReturn(ComponentName(supervisionPackageName, "MainActivity"))
+
+        service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID, preCreated = true))
+
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.app.admin.flags.Flags.FLAG_ENABLE_SUPERVISION_SERVICE_SYNC)
+    fun syncStateWithDevicePolicyManager_supervisionAppIsNotProfileOwner_doesNotEnableSupervision() {
+        whenever(mockDpmInternal.getProfileOwnerAsUser(USER_ID))
+            .thenReturn(ComponentName("other.package", "MainActivity"))
+
+        service.syncStateWithDevicePolicyManager(newTargetUser(USER_ID))
+
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+    }
+
+    @Test
+    fun setSupervisionEnabledForUser() {
         assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
 
         service.setSupervisionEnabledForUser(USER_ID, true)
@@ -72,7 +123,18 @@
     }
 
     @Test
-    fun testSetSupervisionLockscreenEnabledForUser() {
+    fun supervisionEnabledForUser_internal() {
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+        service.mInternal.setSupervisionEnabledForUser(USER_ID, true)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+        service.mInternal.setSupervisionEnabledForUser(USER_ID, false)
+        assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+    }
+
+    @Test
+    fun setSupervisionLockscreenEnabledForUser() {
         var userData = service.getUserDataLocked(USER_ID)
         assertThat(userData.supervisionLockScreenEnabled).isFalse()
         assertThat(userData.supervisionLockScreenOptions).isNull()
@@ -87,4 +149,10 @@
         assertThat(userData.supervisionLockScreenEnabled).isFalse()
         assertThat(userData.supervisionLockScreenOptions).isNull()
     }
+
+    private fun newTargetUser(userId: Int, preCreated: Boolean = false): TargetUser {
+        val userInfo = UserInfo(userId, /* name= */ "tempUser", /* flags= */ 0)
+        userInfo.preCreated = preCreated
+        return TargetUser(userInfo)
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index c3466b9..fee646d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3139,11 +3139,13 @@
 
     @Test
     public void testOnStartingWindowDrawn() {
+        // Skip unnecessary resume top.
+        mSupervisor.beginDeferResume();
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         // The task-has-been-visible should not affect the decision of making transition ready.
         activity.getTask().setHasBeenVisible(true);
         activity.detachFromProcess();
-        activity.mStartingData = mock(StartingData.class);
+        activity.mStartingData = new SplashScreenStartingData(mWm, 0, 0);
         registerTestTransitionPlayer();
         final Transition transition = activity.mTransitionController.requestTransitionIfNeeded(
                 WindowManager.TRANSIT_OPEN, 0 /* flags */, null /* trigger */, mDisplayContent);
@@ -3151,7 +3153,11 @@
         assertTrue(activity.mStartingData.mIsDisplayed);
         // The transition can be ready by the starting window of a visible-requested activity
         // without a running process.
-        assertTrue(transition.allReady());
+        if (!transition.allReady()) {
+            // Print unsatisfied conditions.
+            transition.onReadyTimeout();
+            Assert.fail(transition + " must be ready by onStartingWindowDrawn");
+        }
 
         // If other event makes the transition unready, the reentrant of onStartingWindowDrawn
         // should not replace the readiness again.
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 62a4711..41f1e23 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -87,6 +87,7 @@
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.times;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -4782,6 +4783,114 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testCameraCompatAspectRatioAppliedForFixedOrientationCameraActivities() {
+        // Needed to create camera compat policy in DisplayContent.
+        allowDesktopMode();
+        // Create display that has all stable insets and does not rotate.
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+                .setSystemDecorations(true).setCanRotate(false).build();
+
+        final float cameraCompatAspectRatio = 4.0f;
+        setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+        // Create task on test display.
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+        // Create fixed portrait activity.
+        final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build();
+        final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        assertEquals(cameraCompatAspectRatio, computeAspectRatio(fixedOrientationAppBounds),
+                DELTA_ASPECT_RATIO_TOLERANCE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testCameraCompatAspectRatioForFixedOrientationCameraActivitiesPortraitWindow() {
+        // Needed to create camera compat policy in DisplayContent.
+        allowDesktopMode();
+        // Create portrait display that has all stable insets and does not rotate.
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 1600)
+                .setSystemDecorations(true).setCanRotate(false).build();
+
+        final float cameraCompatAspectRatio = 4.0f;
+        setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+        // Create task on test display.
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+        // Create fixed portrait activity.
+        final ActivityRecord fixedOrientationActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT).build();
+        final Rect fixedOrientationAppBounds = new Rect(fixedOrientationActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        assertEquals(cameraCompatAspectRatio, computeAspectRatio(fixedOrientationAppBounds),
+                DELTA_ASPECT_RATIO_TOLERANCE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testCameraCompatAspectRatioAppliedInsteadOfDefaultAspectRatio() {
+        // Needed to create camera compat policy in DisplayContent.
+        allowDesktopMode();
+        // Create display that has all stable insets and does not rotate.
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+                .setSystemDecorations(true).setCanRotate(false).build();
+
+        final float cameraCompatAspectRatio = 5.0f;
+        setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+        // Create task on test display.
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+        // App's target min aspect ratio - this should not be used, as camera controls aspect ratio.
+        final float targetMinAspectRatio = 4.0f;
+
+        // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio.
+        final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .setMinAspectRatio(targetMinAspectRatio).build();
+        final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        assertEquals(cameraCompatAspectRatio, computeAspectRatio(minAspectRatioAppBounds),
+                DELTA_ASPECT_RATIO_TOLERANCE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testCameraCompatAspectRatio_defualtAspectRatioAppliedWhenGreater() {
+        // Needed to create camera compat policy in DisplayContent.
+        allowDesktopMode();
+        // Create display that has all stable insets and does not rotate.
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1080, 600)
+                .setSystemDecorations(true).setCanRotate(false).build();
+
+        final float cameraCompatAspectRatio = 5.0f;
+        setupCameraCompatAspectRatio(cameraCompatAspectRatio, display);
+
+        // Create task on test display.
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
+
+        // App's target min aspect ratio bigger than camera compat aspect ratio - use that instead.
+        final float targetMinAspectRatio = 6.0f;
+
+        // Create fixed portrait activity with min aspect ratio greater than parent aspect ratio.
+        final ActivityRecord minAspectRatioActivity = new ActivityBuilder(mAtm)
+                .setTask(task).setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .setMinAspectRatio(targetMinAspectRatio).build();
+        final Rect minAspectRatioAppBounds = new Rect(minAspectRatioActivity.getConfiguration()
+                .windowConfiguration.getAppBounds());
+
+        assertEquals(targetMinAspectRatio, computeAspectRatio(minAspectRatioAppBounds),
+                DELTA_ASPECT_RATIO_TOLERANCE);
+    }
+
+    @Test
     public void testUniversalResizeable() {
         mWm.mConstants.mIgnoreActivityOrientationRequest = true;
         setUpApp(mDisplayContent);
@@ -4868,6 +4977,25 @@
         assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
     }
 
+    /**
+     * {@code canEnterDesktopMode} is called when {@link CameraCompatFreeformPolicy} is created in
+     * {@link AppCompatCameraPolicy}.
+     *
+     * <p>{@link #allowDesktopMode()} needs to be called before {@link DisplayContent} is created.
+     */
+    private void allowDesktopMode() {
+        doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
+    }
+
+    private void setupCameraCompatAspectRatio(float cameraCompatAspectRatio,
+            @NonNull DisplayContent display) {
+        CameraCompatFreeformPolicy cameraPolicy = display.mAppCompatCameraPolicy
+                .mCameraCompatFreeformPolicy;
+        spyOn(cameraPolicy);
+        doReturn(true).when(cameraPolicy).shouldCameraCompatControlAspectRatio(any());
+        doReturn(cameraCompatAspectRatio).when(cameraPolicy).getCameraCompatAspectRatio(any());
+    }
+
     private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
         final AppCompatReachabilityOverrides reachabilityOverrides =
                 mActivity.mAppCompatController.getAppCompatReachabilityOverrides();
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 7e3d99a..887b798 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -3511,6 +3511,40 @@
         }
     }
 
+    /**
+     * Inform whether application supports NTN SMS in satellite mode.
+     *
+     * This method is used by default messaging application to inform framework whether it supports
+     * NTN SMS or not.
+     *
+     * Invoking this API will internally result in triggering
+     * {@link android.telephony.TelephonyCallback.CarrierRoamingNtnModeListener
+     * #onCarrierRoamingNtnAvailableServicesChanged(List)} and
+     * {@link android.telephony.TelephonyCallback.CarrierRoamingNtnModeListener
+     * #onCarrierRoamingNtnEligibleStateChanged(boolean)} callbacks.
+     *
+     * @param ntnSmsSupported {@code true} If application supports NTN SMS, else {@code false}.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     * @throws IllegalStateException if the Telephony process is not currently available.
+     * @hide
+     */
+    @RequiresPermission(allOf = {Manifest.permission.SATELLITE_COMMUNICATION,
+            Manifest.permission.SEND_SMS})
+    public void setNtnSmsSupported(boolean ntnSmsSupported) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                telephony.setNtnSmsSupported(ntnSmsSupported);
+            } else {
+                throw new IllegalStateException("telephony service is null.");
+            }
+        } catch (RemoteException ex) {
+            loge("setNtnSmsSupported() RemoteException:" + ex);
+            ex.rethrowAsRuntimeException();
+        }
+    }
+
     @Nullable
     private static ITelephony getITelephony() {
         ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 210200b..a584273 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3497,4 +3497,15 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
     void deprovisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
+
+   /**
+    * Inform whether application supports NTN SMS in satellite mode.
+    *
+    * This method is used by default messaging application to inform framework whether it supports
+    * NTN SMS or not.
+    *
+    * @param ntnSmsSupported {@code true} If application supports NTN SMS, else {@code false}.
+    * @hide
+    */
+    void setNtnSmsSupported(boolean ntnSmsSupported);
 }