Merge "Port stepping animation to FlexClockView" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 497619a..ad84900 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -21,6 +21,7 @@
     java_aconfig_libraries: [
         // !!! KEEP THIS LIST ALPHABETICAL !!!
         "aconfig_mediacodec_flags_java_lib",
+        "aconfig_settingslib_flags_java_lib",
         "aconfig_trade_in_mode_flags_java_lib",
         "android-sdk-flags-java",
         "android.adaptiveauth.flags-aconfig-java",
@@ -1757,3 +1758,19 @@
     ],
     min_sdk_version: "apex_inherit",
 }
+
+// Settings Lib
+aconfig_declarations {
+    name: "aconfig_settingslib_flags",
+    package: "com.android.settingslib.flags",
+    container: "system",
+    srcs: [
+        "packages/SettingsLib/aconfig/settingslib.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "aconfig_settingslib_flags_java_lib",
+    aconfig_declarations: "aconfig_settingslib_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index 26d0d65..48f0928 100644
--- a/Android.bp
+++ b/Android.bp
@@ -220,7 +220,7 @@
         "android.hardware.contexthub-V1.0-java",
         "android.hardware.contexthub-V1.1-java",
         "android.hardware.contexthub-V1.2-java",
-        "android.hardware.contexthub-V3-java",
+        "android.hardware.contexthub-V4-java",
         "android.hardware.gnss-V1.0-java",
         "android.hardware.gnss-V2.1-java",
         "android.hardware.health-V1.0-java-constants",
@@ -399,6 +399,7 @@
         "com.android.sysprop.foldlockbehavior",
         "com.android.sysprop.view",
         "framework-internal-utils",
+        "dynamic_instrumentation_manager_aidl-java",
         // If MimeMap ever becomes its own APEX, then this dependency would need to be removed
         // in favor of an API stubs dependency in java_library "framework" below.
         "mimemap",
diff --git a/api/Android.bp b/api/Android.bp
index 0ac85e2..cdc5cd1 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -127,27 +127,54 @@
     }),
 }
 
+// Create a single file containing the latest released version of the whole
+// Android public API.
+java_genrule {
+    name: "android.api.merged.public.latest",
+    srcs: [
+        ":android.api.combined.public.latest",
+    ],
+    out: ["public-latest.txt"],
+    tools: ["metalava"],
+    cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)",
+}
+
+// Make sure that the Android public API is compatible with the
+// previously released public API.
 java_genrule {
     name: "frameworks-base-api-current-compat",
     srcs: [
-        ":android.api.public.latest",
+        ":android.api.merged.public.latest",
         ":android-incompatibilities.api.public.latest",
         ":frameworks-base-api-current.txt",
     ],
     out: ["updated-baseline.txt"],
     tools: ["metalava"],
     cmd: metalava_cmd +
-        "--check-compatibility:api:released $(location :android.api.public.latest) " +
+        "--check-compatibility:api:released $(location :android.api.merged.public.latest) " +
         "--baseline:compatibility:released $(location :android-incompatibilities.api.public.latest) " +
         "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
         "$(location :frameworks-base-api-current.txt)",
 }
 
+// Create a single file containing the latest released version of the whole
+// Android system API.
+java_genrule {
+    name: "android.api.merged.system.latest",
+    srcs: [
+        ":android.api.combined.system.latest",
+    ],
+    out: ["system-latest.txt"],
+    tools: ["metalava"],
+    cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)",
+}
+
+// Make sure that the Android system API is compatible with the
+// previously released system API.
 java_genrule {
     name: "frameworks-base-api-system-current-compat",
     srcs: [
-        ":android.api.public.latest",
-        ":android.api.system.latest",
+        ":android.api.merged.system.latest",
         ":android-incompatibilities.api.system.latest",
         ":frameworks-base-api-current.txt",
         ":frameworks-base-api-system-current.txt",
@@ -155,20 +182,31 @@
     out: ["updated-baseline.txt"],
     tools: ["metalava"],
     cmd: metalava_cmd +
-        "--check-compatibility:api:released $(location :android.api.public.latest) " +
-        "--check-compatibility:api:released $(location :android.api.system.latest) " +
+        "--check-compatibility:api:released $(location :android.api.merged.system.latest) " +
         "--baseline:compatibility:released $(location :android-incompatibilities.api.system.latest) " +
         "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
         "$(location :frameworks-base-api-current.txt) " +
         "$(location :frameworks-base-api-system-current.txt)",
 }
 
+// Create a single file containing the latest released version of the whole
+// Android module-lib API.
+java_genrule {
+    name: "android.api.merged.module-lib.latest",
+    srcs: [
+        ":android.api.combined.module-lib.latest",
+    ],
+    out: ["module-lib-latest.txt"],
+    tools: ["metalava"],
+    cmd: metalava_cmd + " merge-signatures --format=2.0 $(in) --out $(out)",
+}
+
+// Make sure that the Android module-lib API is compatible with the
+// previously released module-lib API.
 java_genrule {
     name: "frameworks-base-api-module-lib-current-compat",
     srcs: [
-        ":android.api.public.latest",
-        ":android.api.system.latest",
-        ":android.api.module-lib.latest",
+        ":android.api.merged.module-lib.latest",
         ":android-incompatibilities.api.module-lib.latest",
         ":frameworks-base-api-current.txt",
         ":frameworks-base-api-system-current.txt",
@@ -177,9 +215,7 @@
     out: ["updated-baseline.txt"],
     tools: ["metalava"],
     cmd: metalava_cmd +
-        "--check-compatibility:api:released $(location :android.api.public.latest) " +
-        "--check-compatibility:api:released $(location :android.api.system.latest) " +
-        "--check-compatibility:api:released $(location :android.api.module-lib.latest) " +
+        "--check-compatibility:api:released $(location :android.api.merged.module-lib.latest) " +
         "--baseline:compatibility:released $(location :android-incompatibilities.api.module-lib.latest) " +
         "--update-baseline:compatibility:released $(genDir)/updated-baseline.txt " +
         "$(location :frameworks-base-api-current.txt) " +
@@ -194,7 +230,7 @@
     cmd: "$(location merge_zips) $(out) $(in)",
     srcs: [
         ":api-stubs-docs-non-updatable{.exportable}",
-        ":all-modules-public-stubs-source",
+        ":all-modules-public-stubs-source-exportable",
     ],
     visibility: ["//visibility:private"], // Used by make module in //development, mind
 }
diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp
index e8fcf4b..1ebe0cd 100644
--- a/api/ApiDocs.bp
+++ b/api/ApiDocs.bp
@@ -129,7 +129,7 @@
 droidstubs {
     name: "framework-doc-stubs",
     defaults: ["android-non-updatable-doc-stubs-defaults"],
-    srcs: [":all-modules-public-stubs-source"],
+    srcs: [":all-modules-public-stubs-source-exportable"],
     api_levels_module: "api_versions_public",
     aidl: {
         include_dirs: [
diff --git a/api/api.go b/api/api.go
index f32bdc3..5ca24de 100644
--- a/api/api.go
+++ b/api/api.go
@@ -429,8 +429,9 @@
 
 func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) {
 	props := fgProps{}
-	props.Name = proptools.StringPtr("all-modules-public-stubs-source")
-	props.Device_common_srcs = createSrcs(modules, "{.public.stubs.source}")
+	props.Name = proptools.StringPtr("all-modules-public-stubs-source-exportable")
+	transformConfigurableArray(modules, "", ".stubs.source")
+	props.Device_common_srcs = createSrcs(modules, "{.exportable}")
 	props.Visibility = []string{"//frameworks/base"}
 	ctx.CreateModule(android.FileGroupFactory, &props)
 }
diff --git a/cmds/idmap2/libidmap2/ResourceContainer.cpp b/cmds/idmap2/libidmap2/ResourceContainer.cpp
index 3c0e118..57ae354 100644
--- a/cmds/idmap2/libidmap2/ResourceContainer.cpp
+++ b/cmds/idmap2/libidmap2/ResourceContainer.cpp
@@ -17,6 +17,7 @@
 #include "idmap2/ResourceContainer.h"
 
 #include <memory>
+#include <mutex>
 #include <string>
 #include <utility>
 #include <vector>
@@ -296,7 +297,7 @@
 }  // namespace
 
 struct ApkResourceContainer : public TargetResourceContainer, public OverlayResourceContainer {
-  static Result<std::unique_ptr<ApkResourceContainer>> FromPath(const std::string& path);
+  static Result<std::unique_ptr<ApkResourceContainer>> FromPath(std::string path);
 
   // inherited from TargetResourceContainer
   Result<bool> DefinesOverlayable() const override;
@@ -320,6 +321,7 @@
   Result<const ResState*> GetState() const;
   ZipAssetsProvider* GetZipAssets() const;
 
+  mutable std::mutex state_lock_;
   mutable std::variant<std::unique_ptr<ZipAssetsProvider>, ResState> state_;
   std::string path_;
 };
@@ -330,16 +332,17 @@
 }
 
 Result<std::unique_ptr<ApkResourceContainer>> ApkResourceContainer::FromPath(
-    const std::string& path) {
+    std::string path) {
   auto zip_assets = ZipAssetsProvider::Create(path, 0 /* flags */);
   if (zip_assets == nullptr) {
     return Error("failed to load zip assets");
   }
   return std::unique_ptr<ApkResourceContainer>(
-      new ApkResourceContainer(std::move(zip_assets), path));
+      new ApkResourceContainer(std::move(zip_assets), std::move(path)));
 }
 
 Result<const ResState*> ApkResourceContainer::GetState() const {
+  std::lock_guard lock(state_lock_);
   if (auto state = std::get_if<ResState>(&state_); state != nullptr) {
     return state;
   }
@@ -355,6 +358,7 @@
 }
 
 ZipAssetsProvider* ApkResourceContainer::GetZipAssets() const {
+  std::lock_guard lock(state_lock_);
   if (auto zip = std::get_if<std::unique_ptr<ZipAssetsProvider>>(&state_); zip != nullptr) {
     return zip->get();
   }
@@ -427,7 +431,7 @@
 
 Result<std::unique_ptr<TargetResourceContainer>> TargetResourceContainer::FromPath(
     std::string path) {
-  auto result = ApkResourceContainer::FromPath(path);
+  auto result = ApkResourceContainer::FromPath(std::move(path));
   if (!result) {
     return result.GetError();
   }
@@ -438,7 +442,7 @@
     std::string path) {
   // Load the path as a fabricated overlay if the file magic indicates this is a fabricated overlay.
   if (android::IsFabricatedOverlay(path)) {
-    auto result = FabricatedOverlayContainer::FromPath(path);
+    auto result = FabricatedOverlayContainer::FromPath(std::move(path));
     if (!result) {
       return result.GetError();
     }
@@ -446,7 +450,7 @@
   }
 
   // Fallback to loading the container as an APK.
-  auto result = ApkResourceContainer::FromPath(path);
+  auto result = ApkResourceContainer::FromPath(std::move(path));
   if (!result) {
     return result.GetError();
   }
diff --git a/core/api/current.txt b/core/api/current.txt
index ead6554..e551789 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -263,6 +263,7 @@
     field public static final String READ_SMS = "android.permission.READ_SMS";
     field public static final String READ_SYNC_SETTINGS = "android.permission.READ_SYNC_SETTINGS";
     field public static final String READ_SYNC_STATS = "android.permission.READ_SYNC_STATS";
+    field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String READ_SYSTEM_PREFERENCES = "android.permission.READ_SYSTEM_PREFERENCES";
     field public static final String READ_VOICEMAIL = "com.android.voicemail.permission.READ_VOICEMAIL";
     field public static final String REBOOT = "android.permission.REBOOT";
     field public static final String RECEIVE_BOOT_COMPLETED = "android.permission.RECEIVE_BOOT_COMPLETED";
@@ -313,6 +314,7 @@
     field public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW";
     field public static final String TRANSMIT_IR = "android.permission.TRANSMIT_IR";
     field public static final String TURN_SCREEN_ON = "android.permission.TURN_SCREEN_ON";
+    field @FlaggedApi("android.app.enable_tv_implicit_enter_pip_restriction") public static final String TV_IMPLICIT_ENTER_PIP = "android.permission.TV_IMPLICIT_ENTER_PIP";
     field public static final String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT";
     field public static final String UPDATE_DEVICE_STATS = "android.permission.UPDATE_DEVICE_STATS";
     field public static final String UPDATE_PACKAGES_WITHOUT_USER_ACTION = "android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION";
@@ -334,6 +336,7 @@
     field public static final String WRITE_SECURE_SETTINGS = "android.permission.WRITE_SECURE_SETTINGS";
     field public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
     field public static final String WRITE_SYNC_SETTINGS = "android.permission.WRITE_SYNC_SETTINGS";
+    field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES";
     field public static final String WRITE_VOICEMAIL = "com.android.voicemail.permission.WRITE_VOICEMAIL";
   }
 
@@ -475,6 +478,8 @@
     field public static final int alpha = 16843551; // 0x101031f
     field public static final int alphabeticModifiers = 16844110; // 0x101054e
     field public static final int alphabeticShortcut = 16843235; // 0x10101e3
+    field @FlaggedApi("android.content.pm.change_launcher_badging") public static final int alternateLauncherIcons;
+    field @FlaggedApi("android.content.pm.change_launcher_badging") public static final int alternateLauncherLabels;
     field public static final int alwaysDrawnWithCache = 16842991; // 0x10100ef
     field public static final int alwaysRetainTaskState = 16843267; // 0x1010203
     field @Deprecated public static final int amPmBackgroundColor = 16843941; // 0x10104a5
@@ -8787,8 +8792,31 @@
 
 package android.app.appfunctions {
 
+  @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionException extends java.lang.Exception implements android.os.Parcelable {
+    ctor public AppFunctionException(int, @Nullable String);
+    ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle);
+    method public int describeContents();
+    method public int getErrorCategory();
+    method public int getErrorCode();
+    method @Nullable public String getErrorMessage();
+    method @NonNull public android.os.Bundle getExtras();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.AppFunctionException> CREATOR;
+    field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
+    field public static final int ERROR_CANCELLED = 2001; // 0x7d1
+    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
+    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
+    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
+    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
+    field public static final int ERROR_DENIED = 1000; // 0x3e8
+    field public static final int ERROR_DISABLED = 1002; // 0x3ea
+    field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
+    field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
+    field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
+  }
+
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager {
-    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>);
     method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
     method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
     method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
@@ -8800,7 +8828,7 @@
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+    method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>);
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
 
@@ -8822,30 +8850,14 @@
   }
 
   @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionResponse implements android.os.Parcelable {
+    ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument);
+    ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument, @NonNull android.os.Bundle);
     method public int describeContents();
-    method public int getErrorCategory();
-    method @Nullable public String getErrorMessage();
     method @NonNull public android.os.Bundle getExtras();
-    method public int getResultCode();
     method @NonNull public android.app.appsearch.GenericDocument getResultDocument();
-    method public boolean isSuccess();
-    method @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle);
-    method @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.ExecuteAppFunctionResponse> CREATOR;
-    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
-    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
-    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
-    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
-    field public static final String PROPERTY_RETURN_VALUE = "returnValue";
-    field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8
-    field public static final int RESULT_CANCELLED = 2001; // 0x7d1
-    field public static final int RESULT_DENIED = 1000; // 0x3e8
-    field public static final int RESULT_DISABLED = 1002; // 0x3ea
-    field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb
-    field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9
-    field public static final int RESULT_OK = 0; // 0x0
-    field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0
+    field public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue";
   }
 
 }
@@ -8868,6 +8880,7 @@
     method public void setWebUri(android.net.Uri);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR;
+    field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA";
   }
 
   public class AssistStructure implements android.os.Parcelable {
@@ -19770,6 +19783,8 @@
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0; // 0x0
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_ON = 1; // 0x1
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION = 2; // 0x2
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_AUTO = 0; // 0x0
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; // 0x1
     field public static final int DISTORTION_CORRECTION_MODE_FAST = 1; // 0x1
     field public static final int DISTORTION_CORRECTION_MODE_HIGH_QUALITY = 2; // 0x2
     field public static final int DISTORTION_CORRECTION_MODE_OFF = 0; // 0x0
@@ -19777,6 +19792,9 @@
     field public static final int EDGE_MODE_HIGH_QUALITY = 2; // 0x2
     field public static final int EDGE_MODE_OFF = 0; // 0x0
     field public static final int EDGE_MODE_ZERO_SHUTTER_LAG = 3; // 0x3
+    field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_OFF = 1; // 0x1
+    field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_ON = 2; // 0x2
+    field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN = 0; // 0x0
     field public static final int FLASH_MODE_OFF = 0; // 0x0
     field public static final int FLASH_MODE_SINGLE = 1; // 0x1
     field public static final int FLASH_MODE_TORCH = 2; // 0x2
@@ -19974,6 +19992,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SCENE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_ZOOM_METHOD;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
@@ -20072,10 +20091,12 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SCENE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_ZOOM_METHOD;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_CURRENT_TYPE;
+    field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_NIGHT_MODE_INDICATOR;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_STRENGTH;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_STATE;
@@ -22882,6 +22903,7 @@
     method public void onCryptoError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CryptoException);
     method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException);
     method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int);
+    method @FlaggedApi("android.media.codec.subsession_metrics") public void onMetricsFlushed(@NonNull android.media.MediaCodec, @NonNull android.os.PersistableBundle);
     method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo);
     method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
     method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat);
@@ -23973,6 +23995,7 @@
     field public static final String KEY_MPEGH_COMPATIBLE_SETS = "mpegh-compatible-sets";
     field public static final String KEY_MPEGH_PROFILE_LEVEL_INDICATION = "mpegh-profile-level-indication";
     field public static final String KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT = "mpegh-reference-channel-layout";
+    field @FlaggedApi("android.media.codec.num_input_slots") public static final String KEY_NUM_SLOTS = "num-slots";
     field public static final String KEY_OPERATING_RATE = "operating-rate";
     field public static final String KEY_OUTPUT_REORDER_DEPTH = "output-reorder-depth";
     field public static final String KEY_PCM_ENCODING = "pcm-encoding";
@@ -40371,7 +40394,7 @@
     method @NonNull public android.security.keystore.KeyProtection.Builder setUserPresenceRequired(boolean);
   }
 
-  @FlaggedApi("android.security.keystore_grant_api") public class KeyStoreManager {
+  @FlaggedApi("android.security.keystore_grant_api") public final class KeyStoreManager {
     method @NonNull public java.util.List<java.security.cert.X509Certificate> getGrantedCertificateChainFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
     method @NonNull public java.security.Key getGrantedKeyFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
     method @NonNull public java.security.KeyPair getGrantedKeyPairFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
@@ -41999,6 +42022,193 @@
 
 }
 
+package android.service.settings.preferences {
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class GetValueRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getPreferenceKey();
+    method @NonNull public String getScreenKey();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.GetValueRequest> CREATOR;
+  }
+
+  public static final class GetValueRequest.Builder {
+    ctor public GetValueRequest.Builder(@NonNull String, @NonNull String);
+    method @NonNull public android.service.settings.preferences.GetValueRequest build();
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class GetValueResult implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.service.settings.preferences.SettingsPreferenceMetadata getMetadata();
+    method public int getResultCode();
+    method @Nullable public android.service.settings.preferences.SettingsPreferenceValue getValue();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.GetValueResult> CREATOR;
+    field public static final int RESULT_DISALLOW = 4; // 0x4
+    field public static final int RESULT_INTERNAL_ERROR = 6; // 0x6
+    field public static final int RESULT_INVALID_REQUEST = 5; // 0x5
+    field public static final int RESULT_OK = 0; // 0x0
+    field public static final int RESULT_REQUIRE_APP_PERMISSION = 3; // 0x3
+    field public static final int RESULT_UNAVAILABLE = 2; // 0x2
+    field public static final int RESULT_UNSUPPORTED = 1; // 0x1
+  }
+
+  public static final class GetValueResult.Builder {
+    ctor public GetValueResult.Builder(int);
+    method @NonNull public android.service.settings.preferences.GetValueResult build();
+    method @NonNull public android.service.settings.preferences.GetValueResult.Builder setMetadata(@Nullable android.service.settings.preferences.SettingsPreferenceMetadata);
+    method @NonNull public android.service.settings.preferences.GetValueResult.Builder setValue(@Nullable android.service.settings.preferences.SettingsPreferenceValue);
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class MetadataRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.MetadataRequest> CREATOR;
+  }
+
+  public static final class MetadataRequest.Builder {
+    ctor public MetadataRequest.Builder();
+    method @NonNull public android.service.settings.preferences.MetadataRequest build();
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class MetadataResult implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<android.service.settings.preferences.SettingsPreferenceMetadata> getMetadataList();
+    method public int getResultCode();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.MetadataResult> CREATOR;
+    field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2
+    field public static final int RESULT_OK = 0; // 0x0
+    field public static final int RESULT_UNSUPPORTED = 1; // 0x1
+  }
+
+  public static final class MetadataResult.Builder {
+    ctor public MetadataResult.Builder(int);
+    method @NonNull public android.service.settings.preferences.MetadataResult build();
+    method @NonNull public android.service.settings.preferences.MetadataResult.Builder setMetadataList(@NonNull java.util.List<android.service.settings.preferences.SettingsPreferenceMetadata>);
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SetValueRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getPreferenceKey();
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue getPreferenceValue();
+    method @NonNull public String getScreenKey();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SetValueRequest> CREATOR;
+  }
+
+  public static final class SetValueRequest.Builder {
+    ctor public SetValueRequest.Builder(@NonNull String, @NonNull String, @NonNull android.service.settings.preferences.SettingsPreferenceValue);
+    method @NonNull public android.service.settings.preferences.SetValueRequest build();
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SetValueResult implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getResultCode();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SetValueResult> CREATOR;
+    field public static final int RESULT_DISABLED = 2; // 0x2
+    field public static final int RESULT_DISALLOW = 7; // 0x7
+    field public static final int RESULT_INTERNAL_ERROR = 9; // 0x9
+    field public static final int RESULT_INVALID_REQUEST = 8; // 0x8
+    field public static final int RESULT_OK = 0; // 0x0
+    field public static final int RESULT_REQUIRE_APP_PERMISSION = 5; // 0x5
+    field public static final int RESULT_REQUIRE_USER_CONSENT = 6; // 0x6
+    field public static final int RESULT_RESTRICTED = 3; // 0x3
+    field public static final int RESULT_UNAVAILABLE = 4; // 0x4
+    field public static final int RESULT_UNSUPPORTED = 1; // 0x1
+  }
+
+  public static final class SetValueResult.Builder {
+    ctor public SetValueResult.Builder(int);
+    method @NonNull public android.service.settings.preferences.SetValueResult build();
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceMetadata implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.List<java.lang.String> getBreadcrumbs();
+    method @NonNull public android.os.Bundle getExtras();
+    method @NonNull public String getKey();
+    method @Nullable public android.app.PendingIntent getLaunchIntent();
+    method @NonNull public java.util.List<java.lang.String> getReadPermissions();
+    method @NonNull public String getScreenKey();
+    method @Nullable public String getSummary();
+    method @Nullable public String getTitle();
+    method @NonNull public java.util.List<java.lang.String> getWritePermissions();
+    method public int getWriteSensitivity();
+    method public boolean isAvailable();
+    method public boolean isEnabled();
+    method public boolean isRestricted();
+    method public boolean isWritable();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SettingsPreferenceMetadata> CREATOR;
+    field public static final int INTENT_ONLY = 2; // 0x2
+    field public static final int NOT_SENSITIVE = 0; // 0x0
+    field public static final int SENSITIVE = 1; // 0x1
+  }
+
+  public static final class SettingsPreferenceMetadata.Builder {
+    ctor public SettingsPreferenceMetadata.Builder(@NonNull String, @NonNull String);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata build();
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setAvailable(boolean);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setBreadcrumbs(@NonNull java.util.List<java.lang.String>);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setEnabled(boolean);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setLaunchIntent(@Nullable android.app.PendingIntent);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setReadPermissions(@NonNull java.util.List<java.lang.String>);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setRestricted(boolean);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setSummary(@Nullable String);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setTitle(@Nullable String);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWritable(boolean);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWritePermissions(@NonNull java.util.List<java.lang.String>);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int);
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public abstract class SettingsPreferenceService extends android.app.Service {
+    ctor public SettingsPreferenceService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method public abstract void onGetAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public abstract void onGetPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public abstract void onSetPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    field public static final String ACTION_PREFERENCE_SERVICE = "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String);
+    method public void close();
+    method public void getAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public void getPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public void setPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    method public void start();
+    method public void stop();
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean getBooleanValue();
+    method public double getDoubleValue();
+    method public long getLongValue();
+    method @Nullable public String getStringValue();
+    method public int getType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SettingsPreferenceValue> CREATOR;
+    field public static final int TYPE_BOOLEAN = 0; // 0x0
+    field public static final int TYPE_DOUBLE = 2; // 0x2
+    field public static final int TYPE_LONG = 1; // 0x1
+    field public static final int TYPE_STRING = 3; // 0x3
+  }
+
+  public static final class SettingsPreferenceValue.Builder {
+    ctor public SettingsPreferenceValue.Builder(int);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue build();
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setBooleanValue(boolean);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setDoubleValue(double);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setLongValue(long);
+    method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setStringValue(@Nullable String);
+  }
+
+}
+
 package android.service.textservice {
 
   public abstract class SpellCheckerService extends android.app.Service {
@@ -44104,6 +44314,8 @@
     field public static final String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
     field public static final int CARRIER_NR_AVAILABILITY_NSA = 1; // 0x1
     field public static final int CARRIER_NR_AVAILABILITY_SA = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC = 0; // 0x0
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int CARRIER_ROAMING_NTN_CONNECT_MANUAL = 1; // 0x1
     field public static final int CROSS_SIM_SPN_FORMAT_CARRIER_NAME_ONLY = 0; // 0x0
     field public static final int CROSS_SIM_SPN_FORMAT_CARRIER_NAME_WITH_BRANDING = 1; // 0x1
     field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe
@@ -44174,11 +44386,14 @@
     field public static final String KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY = "carrier_nr_availabilities_int_array";
     field public static final String KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL = "carrier_provisions_wifi_merged_networks_bool";
     field public static final String KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL = "carrier_rcs_provisioning_required_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT = "carrier_roaming_ntn_connect_type_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT = "carrier_roaming_ntn_emergency_call_to_satellite_handover_type_int";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY = "carrier_roaming_satellite_default_services_int_array";
     field public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array";
     field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array";
     field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string";
     field public static final String KEY_CARRIER_SETTINGS_ENABLE_BOOL = "carrier_settings_enable_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT = "carrier_supported_satellite_notification_hysteresis_sec_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE = "carrier_supported_satellite_services_per_provider_bundle";
     field public static final String KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL = "carrier_supports_opp_data_auto_provisioning_bool";
     field public static final String KEY_CARRIER_SUPPORTS_SS_OVER_UT_BOOL = "carrier_supports_ss_over_ut_bool";
@@ -44229,6 +44444,7 @@
     field public static final String KEY_DIAL_STRING_REPLACE_STRING_ARRAY = "dial_string_replace_string_array";
     field public static final String KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL = "disable_cdma_activation_code_bool";
     field public static final String KEY_DISABLE_CHARGE_INDICATION_BOOL = "disable_charge_indication_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL = "disable_dun_apn_while_roaming_with_preset_apn_bool";
     field public static final String KEY_DISABLE_SUPPLEMENTARY_SERVICES_IN_AIRPLANE_MODE_BOOL = "disable_supplementary_services_in_airplane_mode_bool";
     field public static final String KEY_DISCONNECT_CAUSE_PLAY_BUSYTONE_INT_ARRAY = "disconnect_cause_play_busytone_int_array";
     field public static final String KEY_DISPLAY_CALL_STRENGTH_INDICATOR_BOOL = "display_call_strength_indicator_bool";
@@ -44241,6 +44457,8 @@
     field public static final String KEY_EDITABLE_VOICEMAIL_NUMBER_SETTING_BOOL = "editable_voicemail_number_setting_bool";
     field public static final String KEY_EDITABLE_WFC_MODE_BOOL = "editable_wfc_mode_bool";
     field public static final String KEY_EDITABLE_WFC_ROAMING_MODE_BOOL = "editable_wfc_roaming_mode_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT = "emergency_call_to_satellite_t911_handover_timeout_millis_int";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL = "emergency_messaging_supported_bool";
     field public static final String KEY_EMERGENCY_NOTIFICATION_DELAY_INT = "emergency_notification_delay_int";
     field public static final String KEY_EMERGENCY_NUMBER_PREFIX_STRING_ARRAY = "emergency_number_prefix_string_array";
     field public static final String KEY_ENABLE_CROSS_SIM_CALLING_ON_OPPORTUNISTIC_DATA_BOOL = "enable_cross_sim_calling_on_opportunistic_data_bool";
@@ -44287,6 +44505,7 @@
     field public static final String KEY_MMS_MAX_IMAGE_HEIGHT_INT = "maxImageHeight";
     field public static final String KEY_MMS_MAX_IMAGE_WIDTH_INT = "maxImageWidth";
     field public static final String KEY_MMS_MAX_MESSAGE_SIZE_INT = "maxMessageSize";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT = "mms_max_ntn_payload_size_bytes_int";
     field public static final String KEY_MMS_MESSAGE_TEXT_MAX_SIZE_INT = "maxMessageTextSize";
     field public static final String KEY_MMS_MMS_DELIVERY_REPORT_ENABLED_BOOL = "enableMMSDeliveryReports";
     field public static final String KEY_MMS_MMS_ENABLED_BOOL = "enabledMMS";
@@ -44325,6 +44544,7 @@
     field public static final String KEY_OPPORTUNISTIC_NETWORK_EXIT_THRESHOLD_RSSNR_INT = "opportunistic_network_exit_threshold_rssnr_int";
     field public static final String KEY_OPPORTUNISTIC_NETWORK_MAX_BACKOFF_TIME_LONG = "opportunistic_network_max_backoff_time_long";
     field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL = "override_wfc_roaming_mode_while_using_ntn_bool";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT = "parameters_used_for_ntn_lte_signal_bar_int";
     field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
     field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
@@ -44343,6 +44563,7 @@
     field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string";
     field public static final String KEY_READ_ONLY_APN_FIELDS_STRING_ARRAY = "read_only_apn_fields_string_array";
     field public static final String KEY_READ_ONLY_APN_TYPES_STRING_ARRAY = "read_only_apn_types_string_array";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL = "remove_satellite_plmn_in_manual_network_scan_bool";
     field public static final String KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL = "require_entitlement_checks_bool";
     field @Deprecated public static final String KEY_RESTART_RADIO_ON_PDP_FAIL_REGULAR_DEACTIVATION_BOOL = "restart_radio_on_pdp_fail_regular_deactivation_bool";
     field public static final String KEY_RTT_AUTO_UPGRADE_BOOL = "rtt_auto_upgrade_bool";
@@ -44354,13 +44575,18 @@
     field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING = "satellite_entitlement_app_name_string";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING = "satellite_information_redirect_url_string";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_NIDD_APN_NAME_STRING = "satellite_nidd_apn_name_string";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_esos_inactivity_timeout_sec_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_p2p_sms_inactivity_timeout_sec_int";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_P2P_SMS_SUPPORTED_BOOL = "satellite_roaming_p2p_sms_supported_bool";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = "satellite_roaming_screen_off_inactivity_timeout_sec_int";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_ROAMING_TURN_OFF_SESSION_FOR_EMERGENCY_CALL_BOOL = "satellite_roaming_turn_off_session_for_emergency_call_bool";
     field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool";
     field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool";
     field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool";
@@ -44430,6 +44656,9 @@
     field public static final String KEY_WORLD_MODE_ENABLED_BOOL = "world_mode_enabled_bool";
     field public static final String KEY_WORLD_PHONE_BOOL = "world_phone_bool";
     field public static final String REMOVE_GROUP_UUID_STRING = "00000000-0000-0000-0000-000000000000";
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_ALL = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_BANDWIDTH_CONSTRAINED = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED = 0; // 0x0
     field public static final int SERVICE_CLASS_NONE = 0; // 0x0
     field public static final int SERVICE_CLASS_VOICE = 1; // 0x1
     field public static final int USSD_OVER_CS_ONLY = 2; // 0x2
@@ -51799,6 +52028,7 @@
     field public static final int KEYCODE_CHANNEL_DOWN = 167; // 0xa7
     field public static final int KEYCODE_CHANNEL_UP = 166; // 0xa6
     field public static final int KEYCODE_CLEAR = 28; // 0x1c
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_CLOSE = 321; // 0x141
     field public static final int KEYCODE_COMMA = 55; // 0x37
     field public static final int KEYCODE_CONTACTS = 207; // 0xcf
     field public static final int KEYCODE_COPY = 278; // 0x116
@@ -51811,6 +52041,8 @@
     field public static final int KEYCODE_DEMO_APP_2 = 302; // 0x12e
     field public static final int KEYCODE_DEMO_APP_3 = 303; // 0x12f
     field public static final int KEYCODE_DEMO_APP_4 = 304; // 0x130
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_DICTATE = 319; // 0x13f
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_DO_NOT_DISTURB = 322; // 0x142
     field public static final int KEYCODE_DPAD_CENTER = 23; // 0x17
     field public static final int KEYCODE_DPAD_DOWN = 20; // 0x14
     field public static final int KEYCODE_DPAD_DOWN_LEFT = 269; // 0x10d
@@ -51823,7 +52055,7 @@
     field public static final int KEYCODE_DVR = 173; // 0xad
     field public static final int KEYCODE_E = 33; // 0x21
     field public static final int KEYCODE_EISU = 212; // 0xd4
-    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d
+    field public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d
     field public static final int KEYCODE_ENDCALL = 6; // 0x6
     field public static final int KEYCODE_ENTER = 66; // 0x42
     field public static final int KEYCODE_ENVELOPE = 65; // 0x41
@@ -51835,7 +52067,19 @@
     field public static final int KEYCODE_F10 = 140; // 0x8c
     field public static final int KEYCODE_F11 = 141; // 0x8d
     field public static final int KEYCODE_F12 = 142; // 0x8e
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F13 = 326; // 0x146
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F14 = 327; // 0x147
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F15 = 328; // 0x148
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F16 = 329; // 0x149
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F17 = 330; // 0x14a
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F18 = 331; // 0x14b
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F19 = 332; // 0x14c
     field public static final int KEYCODE_F2 = 132; // 0x84
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F20 = 333; // 0x14d
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F21 = 334; // 0x14e
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F22 = 335; // 0x14f
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F23 = 336; // 0x150
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_F24 = 337; // 0x151
     field public static final int KEYCODE_F3 = 133; // 0x85
     field public static final int KEYCODE_F4 = 134; // 0x86
     field public static final int KEYCODE_F5 = 135; // 0x87
@@ -51850,6 +52094,7 @@
     field public static final int KEYCODE_FOCUS = 80; // 0x50
     field public static final int KEYCODE_FORWARD = 125; // 0x7d
     field public static final int KEYCODE_FORWARD_DEL = 112; // 0x70
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_FULLSCREEN = 325; // 0x145
     field public static final int KEYCODE_FUNCTION = 119; // 0x77
     field public static final int KEYCODE_G = 35; // 0x23
     field public static final int KEYCODE_GRAVE = 68; // 0x44
@@ -51873,6 +52118,7 @@
     field public static final int KEYCODE_LANGUAGE_SWITCH = 204; // 0xcc
     field public static final int KEYCODE_LAST_CHANNEL = 229; // 0xe5
     field public static final int KEYCODE_LEFT_BRACKET = 71; // 0x47
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_LOCK = 324; // 0x144
     field public static final int KEYCODE_M = 41; // 0x29
     field public static final int KEYCODE_MACRO_1 = 313; // 0x139
     field public static final int KEYCODE_MACRO_2 = 314; // 0x13a
@@ -51910,6 +52156,7 @@
     field public static final int KEYCODE_NAVIGATE_NEXT = 261; // 0x105
     field public static final int KEYCODE_NAVIGATE_OUT = 263; // 0x107
     field public static final int KEYCODE_NAVIGATE_PREVIOUS = 260; // 0x104
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_NEW = 320; // 0x140
     field public static final int KEYCODE_NOTIFICATION = 83; // 0x53
     field public static final int KEYCODE_NUM = 78; // 0x4e
     field public static final int KEYCODE_NUMPAD_0 = 144; // 0x90
@@ -51944,6 +52191,7 @@
     field public static final int KEYCODE_PLUS = 81; // 0x51
     field public static final int KEYCODE_POUND = 18; // 0x12
     field public static final int KEYCODE_POWER = 26; // 0x1a
+    field @FlaggedApi("com.android.hardware.input.enable_new_25q2_keycodes") public static final int KEYCODE_PRINT = 323; // 0x143
     field public static final int KEYCODE_PROFILE_SWITCH = 288; // 0x120
     field public static final int KEYCODE_PROG_BLUE = 186; // 0xba
     field public static final int KEYCODE_PROG_GREEN = 184; // 0xb8
@@ -51956,7 +52204,7 @@
     field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
     field public static final int KEYCODE_RO = 217; // 0xd9
     field public static final int KEYCODE_S = 47; // 0x2f
-    field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e
+    field public static final int KEYCODE_SCREENSHOT = 318; // 0x13e
     field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74
     field public static final int KEYCODE_SEARCH = 84; // 0x54
     field public static final int KEYCODE_SEMICOLON = 74; // 0x4a
@@ -55410,7 +55658,7 @@
     method public android.os.Bundle getExtras();
     method public CharSequence getHintText();
     method public int getInputType();
-    method public android.view.accessibility.AccessibilityNodeInfo getLabelFor();
+    method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public android.view.accessibility.AccessibilityNodeInfo getLabelFor();
     method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
     method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") @NonNull public java.util.List<android.view.accessibility.AccessibilityNodeInfo> getLabeledByList();
     method public int getLiveRegion();
@@ -55509,8 +55757,8 @@
     method public void setHintText(CharSequence);
     method public void setImportantForAccessibility(boolean);
     method public void setInputType(int);
-    method public void setLabelFor(android.view.View);
-    method public void setLabelFor(android.view.View, int);
+    method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public void setLabelFor(android.view.View);
+    method @Deprecated @FlaggedApi("android.view.accessibility.deprecate_ani_label_for_apis") public void setLabelFor(android.view.View, int);
     method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void setLabeledBy(android.view.View);
     method @Deprecated @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void setLabeledBy(android.view.View, int);
     method public void setLiveRegion(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 2a01ca0..8954f8e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -138,6 +138,7 @@
     field public static final String DISABLE_SYSTEM_SOUND_EFFECTS = "android.permission.DISABLE_SYSTEM_SOUND_EFFECTS";
     field public static final String DISPATCH_PROVISIONING_MESSAGE = "android.permission.DISPATCH_PROVISIONING_MESSAGE";
     field public static final String DOMAIN_VERIFICATION_AGENT = "android.permission.DOMAIN_VERIFICATION_AGENT";
+    field @FlaggedApi("com.android.art.flags.executable_method_file_offsets") public static final String DYNAMIC_INSTRUMENTATION = "android.permission.DYNAMIC_INSTRUMENTATION";
     field @FlaggedApi("com.android.window.flags.untrusted_embedding_any_app_permission") public static final String EMBED_ANY_APP_IN_UNTRUSTED_MODE = "android.permission.EMBED_ANY_APP_IN_UNTRUSTED_MODE";
     field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES";
     field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED";
@@ -389,6 +390,7 @@
     field public static final String START_CROSS_PROFILE_ACTIVITIES = "android.permission.START_CROSS_PROFILE_ACTIVITIES";
     field public static final String START_REVIEW_PERMISSION_DECISIONS = "android.permission.START_REVIEW_PERMISSION_DECISIONS";
     field public static final String START_TASKS_FROM_RECENTS = "android.permission.START_TASKS_FROM_RECENTS";
+    field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String START_VIBRATION_SESSIONS = "android.permission.START_VIBRATION_SESSIONS";
     field public static final String STATUS_BAR_SERVICE = "android.permission.STATUS_BAR_SERVICE";
     field public static final String STOP_APP_SWITCHES = "android.permission.STOP_APP_SWITCHES";
     field public static final String SUBSTITUTE_NOTIFICATION_APP_NAME = "android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME";
@@ -705,6 +707,7 @@
     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 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";
@@ -11660,8 +11663,11 @@
   public abstract class Vibrator {
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener);
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorEffectsSupported();
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorSessionsSupported();
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public boolean isVibrating();
     method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
+    method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @RequiresPermission(allOf={android.Manifest.permission.VIBRATE, android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, android.Manifest.permission.START_VIBRATION_SESSIONS}) public void startVendorSession(@NonNull android.os.VibrationAttributes, @Nullable String, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.vibrator.VendorVibrationSession.Callback);
   }
 
   public static interface Vibrator.OnVibratorStateChangedListener {
@@ -11813,6 +11819,28 @@
 
 }
 
+package android.os.vibrator {
+
+  @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public final class VendorVibrationSession implements java.lang.AutoCloseable {
+    method public void cancel();
+    method public void close();
+    method @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(@NonNull android.os.VibrationEffect, @Nullable String);
+    field public static final int STATUS_CANCELED = 4; // 0x4
+    field public static final int STATUS_IGNORED = 2; // 0x2
+    field public static final int STATUS_SUCCESS = 1; // 0x1
+    field public static final int STATUS_UNKNOWN = 0; // 0x0
+    field public static final int STATUS_UNKNOWN_ERROR = 5; // 0x5
+    field public static final int STATUS_UNSUPPORTED = 3; // 0x3
+  }
+
+  public static interface VendorVibrationSession.Callback {
+    method public void onFinished(int);
+    method public void onFinishing();
+    method public void onStarted(@NonNull android.os.vibrator.VendorVibrationSession);
+  }
+
+}
+
 package android.os.vibrator.persistence {
 
   @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
@@ -18305,7 +18333,12 @@
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForModemStateChanged(@NonNull android.telephony.satellite.SatelliteModemStateCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForNtnSignalStrengthChanged(@NonNull android.telephony.satellite.NtnSignalStrengthCallback);
     method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback);
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7; // 0x7
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3; // 0x3
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5; // 0x5
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4; // 0x4
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int DATAGRAM_TYPE_SMS = 6; // 0x6
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; // 0x1
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DATAGRAM_TYPE_UNKNOWN = 0; // 0x0
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int DEVICE_HOLD_POSITION_LANDSCAPE_LEFT = 2; // 0x2
@@ -18325,6 +18358,7 @@
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; // 0x2
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; // 0x1
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER = 0; // 0x0
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6; // 0x6
@@ -18338,6 +18372,8 @@
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; // 0x7
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3; // 0x3
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2; // 0x2
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9; // 0x9
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8; // 0x8
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_IDLE = 0; // 0x0
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_LISTENING = 1; // 0x1
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6; // 0x6
@@ -18345,11 +18381,16 @@
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5; // 0x5
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; // 0x10
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28; // 0x1c
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS = 27; // 0x1b
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29; // 0x1d
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ERROR = 1; // 0x1
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; // 0x17
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; // 0x8
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; // 0x7
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_LOCATION_DISABLED = 25; // 0x19
+    field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26; // 0x1a
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_BUSY = 22; // 0x16
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_ERROR = 4; // 0x4
     field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; // 0x18
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1173519..3ca55b9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3247,6 +3247,14 @@
 
 }
 
+package android.service.settings.preferences {
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String, boolean, @Nullable android.content.ServiceConnection);
+  }
+
+}
+
 package android.service.voice {
 
   public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector {
@@ -3729,7 +3737,7 @@
     method public final int getDisplayId();
     method public final void setDisplayId(int);
     field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
-    field public static final int LAST_KEYCODE = 318; // 0x13e
+    field public static final int LAST_KEYCODE = 337; // 0x151
   }
 
   public final class KeyboardShortcutGroup implements android.os.Parcelable {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 71623c5..cf5ebbaa3 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -28,6 +28,7 @@
     exclude_srcs: [
         "android/os/*MessageQueue/**/*.java",
         "android/ranging/**/*.java",
+        ":dynamic_instrumentation_manager_aidl_sources",
     ],
     visibility: ["//frameworks/base"],
 }
@@ -120,6 +121,17 @@
 }
 
 filegroup {
+    name: "dynamic_instrumentation_manager_aidl_sources",
+    srcs: ["android/os/instrumentation/*.aidl"],
+}
+
+aidl_interface {
+    name: "dynamic_instrumentation_manager_aidl",
+    srcs: [":dynamic_instrumentation_manager_aidl_sources"],
+    unstable: true,
+}
+
+filegroup {
     name: "framework-internal-display-sources",
     srcs: ["com/android/internal/display/BrightnessSynchronizer.java"],
     visibility: ["//frameworks/base/services/tests/mockingservicestests"],
@@ -685,16 +697,31 @@
 // Generates com.android.internal.pm.RoSystemFeatures, optionally compiling in
 // details about fixed system features defined by build flags. When disabled,
 // the APIs are simply passthrough stubs with no meaningful side effects.
+// TODO(b/203143243): Implement the `--feature=` aggregation  directly with a native soong module.
 genrule {
     name: "systemfeatures-gen-srcs",
     cmd: "$(location systemfeatures-gen-tool) com.android.internal.pm.RoSystemFeatures " +
         // --readonly=false (default) makes the codegen an effective no-op passthrough API.
         " --readonly=" + gen_readonly_feature_apis +
-        // For now, only export "android.hardware.type.*" system features APIs.
-        // TODO(b/203143243): Use an intermediate soong var that aggregates all declared
-        // RELEASE_SYSTEM_FEATURE_* declarations into a single arg.
-        " --feature-apis=AUTOMOTIVE,WATCH,TELEVISION,EMBEDDED,PC" +
-        " > $(out)",
+        " --feature=AUTOMOTIVE:" + select(release_flag("RELEASE_SYSTEM_FEATURE_AUTOMOTIVE"), {
+            any @ value: value,
+            default: "",
+        }) + " --feature=EMBEDDED:" + select(release_flag("RELEASE_SYSTEM_FEATURE_EMBEDDED"), {
+            any @ value: value,
+            default: "",
+        }) + " --feature=LEANBACK:" + select(release_flag("RELEASE_SYSTEM_FEATURE_LEANBACK"), {
+            any @ value: value,
+            default: "",
+        }) + " --feature=PC:" + select(release_flag("RELEASE_SYSTEM_FEATURE_PC"), {
+            any @ value: value,
+            default: "",
+        }) + " --feature=TELEVISION:" + select(release_flag("RELEASE_SYSTEM_FEATURE_TELEVISION"), {
+            any @ value: value,
+            default: "",
+        }) + " --feature=WATCH:" + select(release_flag("RELEASE_SYSTEM_FEATURE_WATCH"), {
+            any @ value: value,
+            default: "",
+        }) + " > $(out)",
     out: [
         "RoSystemFeatures.java",
     ],
diff --git a/core/java/android/adaptiveauth/OWNERS b/core/java/android/adaptiveauth/OWNERS
index 0218a78..bc8efa9 100644
--- a/core/java/android/adaptiveauth/OWNERS
+++ b/core/java/android/adaptiveauth/OWNERS
@@ -1 +1 @@
-include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file
+include /services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
\ No newline at end of file
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 7a1c759..3fccc17 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -30,6 +30,7 @@
 
 import static java.lang.Character.MIN_VALUE;
 
+import android.Manifest;
 import android.annotation.AnimRes;
 import android.annotation.CallSuper;
 import android.annotation.CallbackExecutor;
@@ -3193,6 +3194,16 @@
         return ActivityTaskManager.getMaxNumPictureInPictureActions(this);
     }
 
+    private boolean isImplicitEnterPipProhibited() {
+        PackageManager pm = getPackageManager();
+        if (android.app.Flags.enableTvImplicitEnterPipRestriction()) {
+            return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+                    && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP,
+                    getPackageName()) == PackageManager.PERMISSION_DENIED;
+        }
+        return false;
+    }
+
     /**
      * @return Whether this device supports picture-in-picture.
      */
@@ -9192,6 +9203,8 @@
         }
         dispatchActivityPreResumed();
 
+        mCanEnterPictureInPicture = true;
+
         mFragments.execPendingActions();
 
         mLastNonConfigurationInstances = null;
@@ -9243,6 +9256,11 @@
             Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performPause:"
                     + mComponent.getClassName());
         }
+
+        if (isImplicitEnterPipProhibited()) {
+            mCanEnterPictureInPicture = false;
+        }
+
         dispatchActivityPrePaused();
         mDoReportFullyDrawn = false;
         mFragments.dispatchPause();
@@ -9265,6 +9283,10 @@
 
     final void performUserLeaving() {
         onUserInteraction();
+
+        if (isImplicitEnterPipProhibited()) {
+            mCanEnterPictureInPicture = false;
+        }
         onUserLeaveHint();
     }
 
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 6879458..009cd72 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -101,7 +101,6 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, value = {
             FLAG_UNDEFINED,
-            FLAG_BASE,
             FLAG_LETTERBOX_EDU_ENABLED,
             FLAG_ELIGIBLE_FOR_LETTERBOX_EDU,
             FLAG_LETTERBOXED,
@@ -115,6 +114,10 @@
     })
     public @interface TopActivityFlag {}
 
+    /**
+     * A combination of {@link TopActivityFlag}s that have been enabled through
+     * {@link #setTopActivityFlag}.
+     */
     @TopActivityFlag
     private int mTopActivityFlags;
 
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 38c8583..fd70f4f 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1621,9 +1621,12 @@
      */
     public static final int OP_RANGING = AppOpEnums.APP_OP_RANGING;
 
+    /** @hide Access to read oxygen saturation. */
+    public static final int OP_READ_OXYGEN_SATURATION = AppOpEnums.APP_OP_READ_OXYGEN_SATURATION;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 152;
+    public static final int _NUM_OP = 153;
 
     /**
      * All app ops represented as strings.
@@ -1779,6 +1782,7 @@
             OPSTR_READ_HEART_RATE,
             OPSTR_READ_SKIN_TEMPERATURE,
             OPSTR_RANGING,
+            OPSTR_READ_OXYGEN_SATURATION,
     })
     public @interface AppOpString {}
 
@@ -2521,6 +2525,11 @@
     @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED)
     public static final String OPSTR_READ_HEART_RATE = "android:read_heart_rate";
 
+    /** @hide Access to read oxygen saturation. */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_PLATFORM_OXYGEN_SATURATION_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)
@@ -2608,6 +2617,7 @@
             // 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,
     };
 
     /**
@@ -3129,6 +3139,11 @@
             .setPermission(Flags.rangingPermissionEnabled()?
                 Manifest.permission.RANGING : null)
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_OXYGEN_SATURATION, OPSTR_READ_OXYGEN_SATURATION,
+            "READ_OXYGEN_SATURATION").setPermission(
+                Flags.platformOxygenSaturationEnabled()
+                    ? HealthPermissions.READ_OXYGEN_SATURATION : null)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 2cf718e..3ae60d71 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1167,6 +1167,7 @@
     @Override
     public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
         try {
+            intent.collectExtraIntentKeys();
             ActivityTaskManager.getService().startActivityAsUser(
                     mMainThread.getApplicationThread(), getOpPackageName(), getAttributionTag(),
                     intent, intent.resolveTypeIfNeeded(getContentResolver()),
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c6c0395..0381ee0 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -155,7 +155,7 @@
             FOREGROUND_SERVICE_IMMEDIATE,
             FOREGROUND_SERVICE_DEFERRED
     })
-    public @interface ServiceNotificationPolicy {};
+    public @interface ServiceNotificationPolicy {}
 
     /**
      * If the Notification associated with starting a foreground service has been
@@ -1754,10 +1754,6 @@
     private Icon mSmallIcon;
     @UnsupportedAppUsage
     private Icon mLargeIcon;
-    private Icon mAppIcon;
-
-    /** Cache for whether the notification was posted by a headless system app. */
-    private Boolean mBelongsToHeadlessSystemApp = null;
 
     @UnsupportedAppUsage
     private String mChannelId;
@@ -3247,86 +3243,6 @@
     }
 
     /**
-     * Whether this notification was posted by a headless system app.
-     *
-     * If we don't have enough information to figure this out, this will return false. Therefore,
-     * false negatives are possible, but false positives should not be.
-     *
-     * @hide
-     */
-    public boolean belongsToHeadlessSystemApp(Context context) {
-        Trace.beginSection("Notification#belongsToHeadlessSystemApp");
-
-        try {
-            if (mBelongsToHeadlessSystemApp != null) {
-                return mBelongsToHeadlessSystemApp;
-            }
-
-            if (context == null) {
-                // Without a valid context, we don't know exactly. Let's assume it doesn't belong to
-                // a system app, but not cache the value.
-                return false;
-            }
-
-            ApplicationInfo info = getApplicationInfo(context);
-            if (info != null) {
-                if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
-                    // It's not a system app at all.
-                    mBelongsToHeadlessSystemApp = false;
-                } else {
-                    // If there's no launch intent, it's probably a headless app.
-                    final PackageManager pm = context.getPackageManager();
-                    mBelongsToHeadlessSystemApp = pm.getLaunchIntentForPackage(info.packageName)
-                            == null;
-                }
-            } else {
-                // If for some reason we don't have the app info, we don't know; best assume it's
-                // not a system app.
-                return false;
-            }
-            return mBelongsToHeadlessSystemApp;
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    /**
-     * Get the resource ID of the app icon from application info.
-     * @hide
-     */
-    public int getHeaderAppIconRes(Context context) {
-        ApplicationInfo info = getApplicationInfo(context);
-        if (info != null) {
-            return info.icon;
-        }
-        return 0;
-    }
-
-    /**
-     * Load the app icon drawable from the package manager. This could result in a binder call.
-     * @hide
-     */
-    public Drawable loadHeaderAppIcon(Context context) {
-        Trace.beginSection("Notification#loadHeaderAppIcon");
-
-        try {
-            if (context == null) {
-                Log.e(TAG, "Cannot load the app icon drawable with a null context");
-                return null;
-            }
-            final PackageManager pm = context.getPackageManager();
-            ApplicationInfo info = getApplicationInfo(context);
-            if (info == null) {
-                Log.e(TAG, "Cannot load the app icon drawable: no application info");
-                return null;
-            }
-            return pm.getApplicationIcon(info);
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    /**
      * Fetch the application info from the notification, or the context if that isn't available.
      */
     private ApplicationInfo getApplicationInfo(Context context) {
@@ -4361,55 +4277,6 @@
     }
 
     /**
-     * The colored app icon that can replace the small icon in the notification starting in V.
-     *
-     * Before using this value, you should first check whether it's actually being used by the
-     * notification by calling {@link Notification#shouldUseAppIcon()}.
-     *
-     * @hide
-     */
-    public Icon getAppIcon() {
-        if (mAppIcon != null) {
-            return mAppIcon;
-        }
-        // If the app icon hasn't been loaded yet, check if we can load it without a context.
-        if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) {
-            final ApplicationInfo info = extras.getParcelable(
-                    EXTRA_BUILDER_APPLICATION_INFO,
-                    ApplicationInfo.class);
-            if (info != null) {
-                int appIconRes = info.icon;
-                if (appIconRes == 0) {
-                    Log.w(TAG, "Failed to get the app icon: no icon in application info");
-                    return null;
-                }
-                mAppIcon = Icon.createWithResource(info.packageName, appIconRes);
-                return mAppIcon;
-            } else {
-                Log.e(TAG, "Failed to get the app icon: "
-                        + "there's an EXTRA_BUILDER_APPLICATION_INFO in extras but it's null");
-            }
-        } else {
-            Log.w(TAG, "Failed to get the app icon: no application info in extras");
-        }
-        return null;
-    }
-
-    /**
-     * Whether the notification is using the app icon instead of the small icon.
-     * @hide
-     */
-    public boolean shouldUseAppIcon() {
-        if (Flags.notificationsUseAppIconInRow()) {
-            if (belongsToHeadlessSystemApp(/* context = */ null)) {
-                return false;
-            }
-            return getAppIcon() != null;
-        }
-        return false;
-    }
-
-    /**
      * The large icon shown in this notification's content view.
      * @see Builder#getLargeIcon()
      * @see Builder#setLargeIcon(Icon)
@@ -4566,19 +4433,6 @@
         private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY =
                 SystemProperties.getBoolean("notifications.only_title", true);
 
-        /**
-         * The lightness difference that has to be added to the primary text color to obtain the
-         * secondary text color when the background is light.
-         */
-        private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20;
-
-        /**
-         * The lightness difference that has to be added to the primary text color to obtain the
-         * secondary text color when the background is dark.
-         * A bit less then the above value, since it looks better on dark backgrounds.
-         */
-        private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10;
-
         private Context mContext;
         private Notification mN;
         private Bundle mUserExtras = new Bundle();
@@ -6451,36 +6305,12 @@
         }
 
         private void bindSmallIcon(RemoteViews contentView, StandardTemplateParams p) {
-            if (Flags.notificationsUseAppIcon()) {
-                // Override small icon with app icon
-                mN.mSmallIcon = Icon.createWithResource(mContext,
-                        mN.getHeaderAppIconRes(mContext));
-            } else if (mN.mSmallIcon == null && mN.icon != 0) {
+            if (mN.mSmallIcon == null && mN.icon != 0) {
                 mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon);
             }
-
-            boolean usingAppIcon = false;
-            if (Flags.notificationsUseAppIconInRow() && !mN.belongsToHeadlessSystemApp(mContext)) {
-                // Use the app icon in the view
-                int appIconRes = mN.getHeaderAppIconRes(mContext);
-                if (appIconRes != 0) {
-                    mN.mAppIcon = Icon.createWithResource(mContext, appIconRes);
-                    contentView.setImageViewIcon(R.id.icon, mN.mAppIcon);
-                    contentView.setBoolean(R.id.icon, "setShouldShowAppIcon", true);
-                    usingAppIcon = true;
-                } else {
-                    Log.w(TAG, "bindSmallIcon: could not get the app icon");
-                }
-            }
-            if (!usingAppIcon) {
-                contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon);
-            }
+            contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon);
             contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel);
-
-            // Don't change color if we're using the app icon.
-            if (!Flags.notificationsUseAppIcon() && !usingAppIcon) {
-                processSmallIconColor(mN.mSmallIcon, contentView, p);
-            }
+            processSmallIconColor(mN.mSmallIcon, contentView, p);
         }
 
         /**
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index f432a22..1dc7742 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -32,7 +32,10 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -92,9 +95,6 @@
          * caching on behalf of other processes.
          */
         public boolean shouldBypassCache(@NonNull Q query) {
-            if(android.multiuser.Flags.propertyInvalidatedCacheBypassMismatchedUids()) {
-                return Binder.getCallingUid() != Process.myUid();
-            }
             return false;
         }
     };
@@ -392,8 +392,213 @@
         }
     }
 
+    /**
+     * An array of hash maps, indexed by calling UID.  The class behaves a bit like a hash map
+     * except that it uses the calling UID internally.
+     */
+    private class CacheMap<Query, Result> {
+
+        // Create a new map for a UID, using the parent's configuration for max size.
+        private LinkedHashMap<Query, Result> createMap() {
+            return new LinkedHashMap<Query, Result>(
+                2 /* start small */,
+                0.75f /* default load factor */,
+                true /* LRU access order */) {
+                @GuardedBy("mLock")
+                @Override
+                protected boolean removeEldestEntry(Map.Entry eldest) {
+                    final int size = size();
+                    if (size > mHighWaterMark) {
+                        mHighWaterMark = size;
+                    }
+                    if (size > mMaxEntries) {
+                        mMissOverflow++;
+                        return true;
+                    }
+                    return false;
+                }
+            };
+        }
+
+        // An array of maps, indexed by UID.
+        private final SparseArray<LinkedHashMap<Query, Result>> mCache = new SparseArray<>();
+
+        // If true, isolate the hash entries by calling UID.  If this is false, allow the cache
+        // entries to be combined in a single hash map.
+        private final boolean mIsolated;
+
+        // Collect statistics.
+        private final boolean mStatistics;
+
+        // An array of booleans to indicate if a UID has been involved in a map access.  A value
+        // exists for every UID that was ever involved during cache access. This is updated only
+        // if statistics are being collected.
+        private final SparseBooleanArray mUidSeen;
+
+        // A hash map that ignores the UID.  This is used in look-aside fashion just for hit/miss
+        // statistics.  This is updated only if statistics are being collected.
+        private final ArraySet<Query> mShadowCache;
+
+        // Shadow statistics.  Only hits and misses need to be recorded.  These are updated only
+        // if statistics are being collected.  The "SelfHits" records hits when the UID is the
+        // process uid.
+        private int mShadowHits;
+        private int mShadowMisses;
+        private int mShadowSelfHits;
+
+        // The process UID.
+        private final int mSelfUid;
+
+        // True in test mode.  In test mode, the cache uses Binder.getWorkSource() as the UID.
+        private final boolean mTestMode;
+
+        /**
+         * Create a CacheMap.  UID isolation is enabled if the input parameter is true and if the
+         * isolation feature is enabled.
+         */
+        CacheMap(boolean isolate, boolean testMode) {
+            mIsolated = Flags.picIsolateCacheByUid() && isolate;
+            mStatistics = Flags.picIsolatedCacheStatistics() && mIsolated;
+            if (mStatistics) {
+                mUidSeen = new SparseBooleanArray();
+                mShadowCache = new ArraySet<>();
+            } else {
+                mUidSeen = null;
+                mShadowCache = null;
+            }
+            mSelfUid = Process.myUid();
+            mTestMode = testMode;
+        }
+
+        // Return the UID for this cache invocation.  If uid isolation is disabled, the value of 0
+        // is returned, which effectively places all entries in a single hash map.
+        private int callerUid() {
+            if (!mIsolated) {
+                return 0;
+            } else if (mTestMode) {
+                return Binder.getCallingWorkSourceUid();
+            } else {
+                return Binder.getCallingUid();
+            }
+        }
+
+        /**
+         * Lookup an entry in the cache.
+         */
+        Result get(Query query) {
+            final int uid = callerUid();
+
+            // Shadow statistics
+            if (mStatistics) {
+                if (mShadowCache.contains(query)) {
+                    mShadowHits++;
+                    if (uid == mSelfUid) {
+                        mShadowSelfHits++;
+                    }
+                } else {
+                    mShadowMisses++;
+                }
+            }
+
+            var map = mCache.get(uid);
+            if (map != null) {
+                return map.get(query);
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Remove an entry from the cache.
+         */
+        void remove(Query query) {
+            final int uid = callerUid();
+            if (mStatistics) {
+                mShadowCache.remove(query);
+            }
+
+            var map = mCache.get(uid);
+            if (map != null) {
+                map.remove(query);
+            }
+        }
+
+        /**
+         * Record an entry in the cache.
+         */
+        void put(Query query, Result result) {
+            final int uid = callerUid();
+            if (mStatistics) {
+                mShadowCache.add(query);
+                mUidSeen.put(uid, true);
+            }
+
+            var map = mCache.get(uid);
+            if (map == null) {
+                map = createMap();
+                mCache.put(uid, map);
+            }
+            map.put(query, result);
+        }
+
+        /**
+         * Return the number of entries in the cache.
+         */
+        int size() {
+            int total = 0;
+            for (int i = 0; i < mCache.size(); i++) {
+                var map = mCache.valueAt(i);
+                total += map.size();
+            }
+            return total;
+        }
+
+        /**
+         * Clear the entries in the cache.  Update the shadow statistics.
+         */
+        void clear() {
+            if (mStatistics) {
+                mShadowCache.clear();
+            }
+
+            mCache.clear();
+        }
+
+        // Dump basic statistics, if any are collected.  Do nothing if statistics are not enabled.
+        void dump(PrintWriter pw) {
+            if (mStatistics) {
+                pw.println(formatSimple("    ShadowHits: %d, ShadowMisses: %d, ShadowSize: %d",
+                                mShadowHits, mShadowMisses, mShadowCache.size()));
+                pw.println(formatSimple("    ShadowUids: %d, SelfUid: %d",
+                                mUidSeen.size(), mShadowSelfHits));
+            }
+        }
+
+        // Dump detailed statistics
+        void dumpDetailed(PrintWriter pw) {
+            for (int i = 0; i < mCache.size(); i++) {
+                int uid = mCache.keyAt(i);
+                var map = mCache.valueAt(i);
+
+                Set<Map.Entry<Query, Result>> cacheEntries = map.entrySet();
+                if (cacheEntries.size() == 0) {
+                    break;
+                }
+
+                pw.println("    Contents:");
+                pw.println(formatSimple("      Uid: %d\n", uid));
+                for (Map.Entry<Query, Result> entry : cacheEntries) {
+                    String key = Objects.toString(entry.getKey());
+                    String value = Objects.toString(entry.getValue());
+
+                    pw.println(formatSimple("      Key: %s\n      Value: %s\n", key, value));
+                }
+            }
+        }
+    }
+
     @GuardedBy("mLock")
-    private final LinkedHashMap<Query, Result> mCache;
+    private final CacheMap<Query, Result> mCache;
 
     /**
      * The nonce handler for this cache.
@@ -895,7 +1100,8 @@
      * is allowed to be null in the record constructor to facility reuse of Args instances.
      * @hide
      */
-    public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries) {
+    public static record Args(@NonNull String mModule, @Nullable String mApi,
+            int mMaxEntries, boolean mIsolateUids, boolean mTestMode) {
 
         // Validation: the module must be one of the known module strings and the maxEntries must
         // be positive.
@@ -909,15 +1115,28 @@
         // which is not legal, but there is no reasonable default.  Clients must call the api
         // method to set the field properly.
         public Args(@NonNull String module) {
-            this(module, /* api */ null, /* maxEntries */ 32);
+            this(module,
+                    null,       // api
+                    32,         // maxEntries
+                    true,       // isolateUids
+                    false       // testMode
+                 );
         }
 
         public Args api(@NonNull String api) {
-            return new Args(mModule, api, mMaxEntries);
+            return new Args(mModule, api, mMaxEntries, mIsolateUids, mTestMode);
         }
 
         public Args maxEntries(int val) {
-            return new Args(mModule, mApi, val);
+            return new Args(mModule, mApi, val, mIsolateUids, mTestMode);
+        }
+
+        public Args isolateUids(boolean val) {
+            return new Args(mModule, mApi, mMaxEntries, val, mTestMode);
+        }
+
+        public Args testMode(boolean val) {
+            return new Args(mModule, mApi, mMaxEntries, mIsolateUids, val);
         }
     }
 
@@ -936,7 +1155,7 @@
         mCacheName = cacheName;
         mNonce = getNonceHandler(mPropertyName);
         mMaxEntries = args.mMaxEntries;
-        mCache = createMap();
+        mCache = new CacheMap<>(args.mIsolateUids, args.mTestMode);
         mComputer = (computer != null) ? computer : new DefaultComputer<>(this);
         registerCache();
     }
@@ -1006,28 +1225,6 @@
         this(new Args(module).maxEntries(maxEntries).api(api), cacheName, computer);
     }
 
-    // Create a map.  This should be called only from the constructor.
-    private LinkedHashMap<Query, Result> createMap() {
-        return new LinkedHashMap<Query, Result>(
-            2 /* start small */,
-            0.75f /* default load factor */,
-            true /* LRU access order */) {
-                @GuardedBy("mLock")
-                @Override
-                protected boolean removeEldestEntry(Map.Entry eldest) {
-                    final int size = size();
-                    if (size > mHighWaterMark) {
-                        mHighWaterMark = size;
-                    }
-                    if (size > mMaxEntries) {
-                        mMissOverflow++;
-                        return true;
-                    }
-                    return false;
-                }
-        };
-    }
-
     /**
      * Register the map in the global list.  If the cache is disabled globally, disable it
      * now.  This method is only ever called from the constructor, which means no other thread has
@@ -1778,8 +1975,8 @@
             pw.println(formatSimple("  Cache Name: %s", cacheName()));
             pw.println(formatSimple("    Property: %s", mPropertyName));
             pw.println(formatSimple(
-                "    Hits: %d, Misses: %d, Skips: %d, Clears: %d",
-                mHits, mMisses, getSkipsLocked(), mClears));
+                "    Hits: %d, Misses: %d, Skips: %d, Clears: %d, Uids: %d",
+                mHits, mMisses, getSkipsLocked(), mClears, mCache.size()));
 
             // Print all the skip reasons.
             pw.format("    Skip-%s: %d", sNonceName[0], mSkips[0]);
@@ -1794,25 +1991,16 @@
             pw.println(formatSimple(
                 "    Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d",
                 mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow));
+            mCache.dump(pw);
             pw.println(formatSimple("    Enabled: %s", mDisabled ? "false" : "true"));
 
-            // No specific cache was requested.  This is the default, and no details
-            // should be dumped.
-            if (!detailed) {
-                return;
-            }
-            Set<Map.Entry<Query, Result>> cacheEntries = mCache.entrySet();
-            if (cacheEntries.size() == 0) {
-                return;
+            // Dump the contents of the cache.
+            if (detailed) {
+                mCache.dumpDetailed(pw);
             }
 
-            pw.println("    Contents:");
-            for (Map.Entry<Query, Result> entry : cacheEntries) {
-                String key = Objects.toString(entry.getKey());
-                String value = Objects.toString(entry.getValue());
-
-                pw.println(formatSimple("      Key: %s\n      Value: %s\n", key, value));
-            }
+            // Separator between caches.
+            pw.println("");
         }
     }
 
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 04a9d13..be24bfa 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -359,3 +359,18 @@
     description: "Enables coexistence support for Setting MTE policy."
     bug: "376213673"
 }
+
+flag {
+    name: "enable_supervision_service_sync"
+    is_exported: true
+    namespace: "enterprise"
+    description: "Allows DPMS to enable or disable SupervisionService based on whether the device is being managed by the supervision role holder."
+    bug: "376213673"
+}
+
+flag {
+  name: "split_create_managed_profile_enabled"
+  namespace: "enterprise"
+  description: "Split up existing create and provision managed profile API."
+  bug: "375382324"
+}
diff --git a/core/java/android/app/appfunctions/AppFunctionException.aidl b/core/java/android/app/appfunctions/AppFunctionException.aidl
new file mode 100644
index 0000000..7d43224
--- /dev/null
+++ b/core/java/android/app/appfunctions/AppFunctionException.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.app.appfunctions;
+
+import android.app.appfunctions.AppFunctionException;
+
+parcelable AppFunctionException;
\ No newline at end of file
diff --git a/core/java/android/app/appfunctions/AppFunctionException.java b/core/java/android/app/appfunctions/AppFunctionException.java
new file mode 100644
index 0000000..cbd1d93
--- /dev/null
+++ b/core/java/android/app/appfunctions/AppFunctionException.java
@@ -0,0 +1,261 @@
+/*
+ * 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.app.appfunctions;
+
+import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Represents an app function related errors. */
+@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+public final class AppFunctionException extends Exception implements Parcelable {
+    /**
+     * The caller does not have the permission to execute an app function.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_DENIED = 1000;
+
+    /**
+     * The caller supplied invalid arguments to the execution request.
+     *
+     * <p>This error may be considered similar to {@link IllegalArgumentException}.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_INVALID_ARGUMENT = 1001;
+
+    /**
+     * The caller tried to execute a disabled app function.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_DISABLED = 1002;
+
+    /**
+     * The caller tried to execute a function that does not exist.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_FUNCTION_NOT_FOUND = 1003;
+
+    /**
+     * An internal unexpected error coming from the system.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
+     */
+    public static final int ERROR_SYSTEM_ERROR = 2000;
+
+    /**
+     * The operation was cancelled. Use this error code to report that a cancellation is done after
+     * receiving a cancellation signal.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
+     */
+    public static final int ERROR_CANCELLED = 2001;
+
+    /**
+     * An unknown error occurred while processing the call in the AppFunctionService.
+     *
+     * <p>This error is thrown when the service is connected in the remote application but an
+     * unexpected error is thrown from the bound application.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_APP} category.
+     */
+    public static final int ERROR_APP_UNKNOWN_ERROR = 3000;
+
+    /**
+     * The error category is unknown.
+     *
+     * <p>This is the default value for {@link #getErrorCategory}.
+     */
+    public static final int ERROR_CATEGORY_UNKNOWN = 0;
+
+    /**
+     * The error is caused by the app requesting a function execution.
+     *
+     * <p>For example, the caller provided invalid parameters in the execution request e.g. an
+     * invalid function ID.
+     *
+     * <p>Errors in the category fall in the range 1000-1999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_REQUEST_ERROR = 1;
+
+    /**
+     * The error is caused by an issue in the system.
+     *
+     * <p>For example, the AppFunctionService implementation is not found by the system.
+     *
+     * <p>Errors in the category fall in the range 2000-2999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_SYSTEM = 2;
+
+    /**
+     * The error is caused by the app providing the function.
+     *
+     * <p>For example, the app crashed when the system is executing the request.
+     *
+     * <p>Errors in the category fall in the range 3000-3999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_APP = 3;
+
+    private final int mErrorCode;
+    @Nullable private final String mErrorMessage;
+    @NonNull private final Bundle mExtras;
+
+    /**
+     * @param errorCode The error code.
+     * @param errorMessage The error message.
+     */
+    public AppFunctionException(@ErrorCode int errorCode, @Nullable String errorMessage) {
+        this(errorCode, errorMessage, Bundle.EMPTY);
+    }
+
+    /**
+     * @param errorCode The error code.
+     * @param errorMessage The error message.
+     * @param extras The extras associated with this error.
+     */
+    public AppFunctionException(
+            @ErrorCode int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) {
+        super(errorMessage);
+        mErrorCode = errorCode;
+        mErrorMessage = errorMessage;
+        mExtras = Objects.requireNonNull(extras);
+    }
+
+    private AppFunctionException(@NonNull Parcel in) {
+        mErrorCode = in.readInt();
+        mErrorMessage = in.readString8();
+        mExtras = Objects.requireNonNull(in.readBundle(getClass().getClassLoader()));
+    }
+
+    /** Returns one of the {@code ERROR} constants. */
+    @ErrorCode
+    public int getErrorCode() {
+        return mErrorCode;
+    }
+
+    /** Returns the error message. */
+    @Nullable
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Returns the error category.
+     *
+     * <p>This method categorizes errors based on their underlying cause, allowing developers to
+     * implement targeted error handling and provide more informative error messages to users. It
+     * maps ranges of error codes to specific error categories.
+     *
+     * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the error code does not belong to
+     * any error category.
+     *
+     * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding
+     * error code ranges.
+     */
+    @ErrorCategory
+    public int getErrorCategory() {
+        if (mErrorCode >= 1000 && mErrorCode < 2000) {
+            return ERROR_CATEGORY_REQUEST_ERROR;
+        }
+        if (mErrorCode >= 2000 && mErrorCode < 3000) {
+            return ERROR_CATEGORY_SYSTEM;
+        }
+        if (mErrorCode >= 3000 && mErrorCode < 4000) {
+            return ERROR_CATEGORY_APP;
+        }
+        return ERROR_CATEGORY_UNKNOWN;
+    }
+
+    /** Returns any extras associated with this error. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mErrorCode);
+        dest.writeString8(mErrorMessage);
+        dest.writeBundle(mExtras);
+    }
+
+    /**
+     * Error codes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"ERROR_"},
+            value = {
+                ERROR_DENIED,
+                ERROR_APP_UNKNOWN_ERROR,
+                ERROR_FUNCTION_NOT_FOUND,
+                ERROR_SYSTEM_ERROR,
+                ERROR_INVALID_ARGUMENT,
+                ERROR_DISABLED,
+                ERROR_CANCELLED
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCode {}
+
+    /**
+     * Error categories.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"ERROR_CATEGORY_"},
+            value = {
+                ERROR_CATEGORY_UNKNOWN,
+                ERROR_CATEGORY_REQUEST_ERROR,
+                ERROR_CATEGORY_APP,
+                ERROR_CATEGORY_SYSTEM
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCategory {}
+
+    @NonNull
+    public static final Creator<AppFunctionException> CREATOR =
+            new Creator<>() {
+                @Override
+                public AppFunctionException createFromParcel(Parcel in) {
+                    return new AppFunctionException(in);
+                }
+
+                @Override
+                public AppFunctionException[] newArray(int size) {
+                    return new AppFunctionException[size];
+                }
+            };
+}
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 5ddb590..ed088fe 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -16,7 +16,7 @@
 
 package android.app.appfunctions;
 
-import static android.app.appfunctions.ExecuteAppFunctionResponse.getResultCode;
+import static android.app.appfunctions.AppFunctionException.ERROR_SYSTEM_ERROR;
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 
 import android.Manifest;
@@ -39,7 +39,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 /**
  * Provides access to app functions.
@@ -147,16 +146,16 @@
      * @param request the request to execute the app function
      * @param executor the executor to run the callback
      * @param cancellationSignal the cancellation signal to cancel the execution.
-     * @param callback the callback to receive the function execution result.
+     * @param callback the callback to receive the function execution result or error.
      *     <p>If the calling app does not own the app function or does not have {@code
      *     android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code
      *     android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code
-     *     ExecuteAppFunctionResponse.RESULT_DENIED}.
+     *     AppFunctionException.ERROR_DENIED}.
      *     <p>If the caller only has {@code android.permission.EXECUTE_APP_FUNCTIONS} but the
      *     function requires {@code android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED}, the execution
-     *     result will contain {@code ExecuteAppFunctionResponse.RESULT_DENIED}
+     *     result will contain {@code AppFunctionException.ERROR_DENIED}
      *     <p>If the function requested for execution is disabled, then the execution result will
-     *     contain {@code ExecuteAppFunctionResponse.RESULT_DISABLED}
+     *     contain {@code AppFunctionException.ERROR_DISABLED}
      *     <p>If the cancellation signal is issued, the operation is cancelled and no response is
      *     returned to the caller.
      */
@@ -171,7 +170,9 @@
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+            @NonNull
+                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
+                            callback) {
         Objects.requireNonNull(request);
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
@@ -186,20 +187,25 @@
                             aidlRequest,
                             new IExecuteAppFunctionCallback.Stub() {
                                 @Override
-                                public void onResult(ExecuteAppFunctionResponse result) {
+                                public void onSuccess(ExecuteAppFunctionResponse result) {
                                     try {
-                                        executor.execute(() -> callback.accept(result));
+                                        executor.execute(() -> callback.onResult(result));
                                     } catch (RuntimeException e) {
                                         // Ideally shouldn't happen since errors are wrapped into
-                                        // the
-                                        // response, but we catch it here for additional safety.
-                                        callback.accept(
-                                                ExecuteAppFunctionResponse.newFailure(
-                                                        getResultCode(e),
-                                                        e.getMessage(),
-                                                        /* extras= */ null));
+                                        // the response, but we catch it here for additional safety.
+                                        executor.execute(
+                                                () ->
+                                                        callback.onError(
+                                                                new AppFunctionException(
+                                                                        ERROR_SYSTEM_ERROR,
+                                                                        e.getMessage())));
                                     }
                                 }
+
+                                @Override
+                                public void onError(AppFunctionException exception) {
+                                    executor.execute(() -> callback.onError(exception));
+                                }
                             });
             if (cancellationTransport != null) {
                 cancellationSignal.setRemote(cancellationTransport);
diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
index 06d95f5..3ddda22 100644
--- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
+++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
@@ -213,9 +213,7 @@
             setEnabled(original.getEnabled());
         }
 
-        /**
-         * Sets an indicator specifying the function enabled state.
-         */
+        /** Sets an indicator specifying the function enabled state. */
         @NonNull
         public Builder setEnabled(@EnabledState int enabledState) {
             if (enabledState != APP_FUNCTION_STATE_DEFAULT
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 63d187a..85b6ab2 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -17,7 +17,6 @@
 package android.app.appfunctions;
 
 import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
-import static android.app.appfunctions.ExecuteAppFunctionResponse.getResultCode;
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 
@@ -32,10 +31,8 @@
 import android.os.CancellationSignal;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
+import android.os.OutcomeReceiver;
 import android.os.RemoteException;
-import android.util.Log;
-
-import java.util.function.Consumer;
 
 /**
  * Abstract base class to provide app functions to the system.
@@ -80,7 +77,9 @@
                 @NonNull ExecuteAppFunctionRequest request,
                 @NonNull String callingPackage,
                 @NonNull CancellationSignal cancellationSignal,
-                @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+                @NonNull
+                        OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
+                                callback);
     }
 
     /** @hide */
@@ -105,13 +104,22 @@
                             request,
                             callingPackage,
                             buildCancellationSignal(cancellationCallback),
-                            safeCallback::onResult);
+                            new OutcomeReceiver<>() {
+                                @Override
+                                public void onResult(ExecuteAppFunctionResponse result) {
+                                    safeCallback.onResult(result);
+                                }
+
+                                @Override
+                                public void onError(AppFunctionException exception) {
+                                    safeCallback.onError(exception);
+                                }
+                            });
                 } catch (Exception ex) {
                     // Apps should handle exceptions. But if they don't, report the error on
                     // behalf of them.
-                    safeCallback.onResult(
-                            ExecuteAppFunctionResponse.newFailure(
-                                    getResultCode(ex), ex.getMessage(), /* extras= */ null));
+                    safeCallback.onError(
+                            new AppFunctionException(toErrorCode(ex), ex.getMessage()));
                 }
             }
         };
@@ -164,12 +172,26 @@
      * @param request The function execution request.
      * @param callingPackage The package name of the app that is requesting the execution.
      * @param cancellationSignal A signal to cancel the execution.
-     * @param callback A callback to report back the result.
+     * @param callback A callback to report back the result or error.
      */
     @MainThread
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull String callingPackage,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+            @NonNull
+                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
+                            callback);
+
+    /**
+     * Returns result codes from throwable.
+     *
+     * @hide
+     */
+    private static @AppFunctionException.ErrorCode int toErrorCode(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return AppFunctionException.ERROR_INVALID_ARGUMENT;
+        }
+        return AppFunctionException.ERROR_APP_UNKNOWN_ERROR;
+    }
 }
diff --git a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
index a23f842..1869d22 100644
--- a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
@@ -38,7 +38,7 @@
     public static final String STATIC_SCHEMA_TYPE = "AppFunctionStaticMetadata";
     public static final String STATIC_PROPERTY_ENABLED_BY_DEFAULT = "enabledByDefault";
     public static final String STATIC_PROPERTY_RESTRICT_CALLERS_WITH_EXECUTE_APP_FUNCTIONS =
-        "restrictCallersWithExecuteAppFunctions";
+            "restrictCallersWithExecuteAppFunctions";
 
     public static final String APP_FUNCTION_STATIC_NAMESPACE = "app_functions";
     public static final String PROPERTY_FUNCTION_ID = "functionId";
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
index 41bb622..1557815 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java
@@ -111,8 +111,8 @@
      * Returns the function parameters. The key is the parameter name, and the value is the
      * parameter value.
      *
-     * <p>The bundle may have missing parameters. Developers are advised to implement defensive
-     * handling measures.
+     * <p>The {@link GenericDocument} may have missing parameters. Developers are advised to
+     * implement defensive handling measures.
      *
      * @see AppFunctionManager on how to determine the expected parameters.
      */
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index cdf02e6..acad43b 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -19,16 +19,12 @@
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 
 import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.appsearch.GenericDocument;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /** The response to an app function execution. */
@@ -45,10 +41,7 @@
                     Bundle extras =
                             Objects.requireNonNull(
                                     parcel.readBundle(Bundle.class.getClassLoader()));
-                    int resultCode = parcel.readInt();
-                    String errorMessage = parcel.readString8();
-                    return new ExecuteAppFunctionResponse(
-                            resultWrapper, extras, resultCode, errorMessage);
+                    return new ExecuteAppFunctionResponse(resultWrapper.getValue(), extras);
                 }
 
                 @Override
@@ -71,113 +64,7 @@
      *
      * <p>See {@link #getResultDocument} for more information on extracting the return value.
      */
-    public static final String PROPERTY_RETURN_VALUE = "returnValue";
-
-    /**
-     * The call was successful.
-     *
-     * <p>This result code does not belong in an error category.
-     */
-    public static final int RESULT_OK = 0;
-
-    /**
-     * The caller does not have the permission to execute an app function.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_DENIED = 1000;
-
-    /**
-     * The caller supplied invalid arguments to the execution request.
-     *
-     * <p>This error may be considered similar to {@link IllegalArgumentException}.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_INVALID_ARGUMENT = 1001;
-
-    /**
-     * The caller tried to execute a disabled app function.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_DISABLED = 1002;
-
-    /**
-     * The caller tried to execute a function that does not exist.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_FUNCTION_NOT_FOUND = 1003;
-
-    /**
-     * An internal unexpected error coming from the system.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
-     */
-    public static final int RESULT_SYSTEM_ERROR = 2000;
-
-    /**
-     * The operation was cancelled. Use this error code to report that a cancellation is done after
-     * receiving a cancellation signal.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
-     */
-    public static final int RESULT_CANCELLED = 2001;
-
-    /**
-     * An unknown error occurred while processing the call in the AppFunctionService.
-     *
-     * <p>This error is thrown when the service is connected in the remote application but an
-     * unexpected error is thrown from the bound application.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_APP} category.
-     */
-    public static final int RESULT_APP_UNKNOWN_ERROR = 3000;
-
-    /**
-     * The error category is unknown.
-     *
-     * <p>This is the default value for {@link #getErrorCategory}.
-     */
-    public static final int ERROR_CATEGORY_UNKNOWN = 0;
-
-    /**
-     * The error is caused by the app requesting a function execution.
-     *
-     * <p>For example, the caller provided invalid parameters in the execution request e.g. an
-     * invalid function ID.
-     *
-     * <p>Errors in the category fall in the range 1000-1999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_REQUEST_ERROR = 1;
-
-    /**
-     * The error is caused by an issue in the system.
-     *
-     * <p>For example, the AppFunctionService implementation is not found by the system.
-     *
-     * <p>Errors in the category fall in the range 2000-2999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_SYSTEM = 2;
-
-    /**
-     * The error is caused by the app providing the function.
-     *
-     * <p>For example, the app crashed when the system is executing the request.
-     *
-     * <p>Errors in the category fall in the range 3000-3999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_APP = 3;
-
-    /** The result code of the app function execution. */
-    @ResultCode private final int mResultCode;
-
-    /**
-     * The error message associated with the result, if any. This is {@code null} if the result code
-     * is {@link #RESULT_OK}.
-     */
-    @Nullable private final String mErrorMessage;
+    public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue";
 
     /**
      * Returns the return value of the executed function.
@@ -192,103 +79,21 @@
     /** Returns the additional metadata data relevant to this function execution response. */
     @NonNull private final Bundle mExtras;
 
-    private ExecuteAppFunctionResponse(
-            @NonNull GenericDocumentWrapper resultDocumentWrapper,
-            @NonNull Bundle extras,
-            @ResultCode int resultCode,
-            @Nullable String errorMessage) {
-        mResultDocumentWrapper = Objects.requireNonNull(resultDocumentWrapper);
-        mExtras = Objects.requireNonNull(extras);
-        mResultCode = resultCode;
-        mErrorMessage = errorMessage;
-    }
-
     /**
-     * Returns result codes from throwable.
-     *
-     * @hide
+     * @param resultDocument The return value of the executed function.
      */
-    @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
-    static @ResultCode int getResultCode(@NonNull Throwable t) {
-        if (t instanceof IllegalArgumentException) {
-            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
-        }
-        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+    public ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument) {
+        this(resultDocument, Bundle.EMPTY);
     }
 
     /**
-     * Returns a successful response.
-     *
      * @param resultDocument The return value of the executed function.
      * @param extras The additional metadata for this function execution response.
      */
-    @NonNull
-    @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
-    public static ExecuteAppFunctionResponse newSuccess(
-            @NonNull GenericDocument resultDocument, @Nullable Bundle extras) {
-        Objects.requireNonNull(resultDocument);
-        Bundle actualExtras = getActualExtras(extras);
-        GenericDocumentWrapper resultDocumentWrapper = new GenericDocumentWrapper(resultDocument);
-
-        return new ExecuteAppFunctionResponse(
-                resultDocumentWrapper, actualExtras, RESULT_OK, /* errorMessage= */ null);
-    }
-
-    /**
-     * Returns a failure response.
-     *
-     * @param resultCode The result code of the app function execution.
-     * @param extras The additional metadata for this function execution response.
-     * @param errorMessage The error message associated with the result, if any.
-     */
-    @NonNull
-    @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
-    public static ExecuteAppFunctionResponse newFailure(
-            @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) {
-        if (resultCode == RESULT_OK) {
-            throw new IllegalArgumentException("resultCode must not be RESULT_OK");
-        }
-        Bundle actualExtras = getActualExtras(extras);
-        GenericDocumentWrapper emptyWrapper =
-                new GenericDocumentWrapper(new GenericDocument.Builder<>("", "", "").build());
-        return new ExecuteAppFunctionResponse(emptyWrapper, actualExtras, resultCode, errorMessage);
-    }
-
-    private static Bundle getActualExtras(@Nullable Bundle extras) {
-        if (extras == null) {
-            return Bundle.EMPTY;
-        }
-        return extras;
-    }
-
-    /**
-     * Returns the error category of the {@link ExecuteAppFunctionResponse}.
-     *
-     * <p>This method categorizes errors based on their underlying cause, allowing developers to
-     * implement targeted error handling and provide more informative error messages to users. It
-     * maps ranges of result codes to specific error categories.
-     *
-     * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to
-     * ensure correct categorization of the failed response.
-     *
-     * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to
-     * any error category, for example, in the case of a successful result with {@link #RESULT_OK}.
-     *
-     * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding
-     * result code ranges.
-     */
-    @ErrorCategory
-    public int getErrorCategory() {
-        if (mResultCode >= 1000 && mResultCode < 2000) {
-            return ERROR_CATEGORY_REQUEST_ERROR;
-        }
-        if (mResultCode >= 2000 && mResultCode < 3000) {
-            return ERROR_CATEGORY_SYSTEM;
-        }
-        if (mResultCode >= 3000 && mResultCode < 4000) {
-            return ERROR_CATEGORY_APP;
-        }
-        return ERROR_CATEGORY_UNKNOWN;
+    public ExecuteAppFunctionResponse(
+            @NonNull GenericDocument resultDocument, @NonNull Bundle extras) {
+        mResultDocumentWrapper = new GenericDocumentWrapper(Objects.requireNonNull(resultDocument));
+        mExtras = Objects.requireNonNull(extras);
     }
 
     /**
@@ -296,9 +101,6 @@
      *
      * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
      *
-     * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed
-     * function does not produce a return value.
-     *
      * <p>Sample code for extracting the return value:
      *
      * <pre>
@@ -324,32 +126,6 @@
         return mExtras;
     }
 
-    /**
-     * Returns {@code true} if {@link #getResultCode} equals {@link
-     * ExecuteAppFunctionResponse#RESULT_OK}.
-     */
-    public boolean isSuccess() {
-        return getResultCode() == RESULT_OK;
-    }
-
-    /**
-     * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}.
-     */
-    @ResultCode
-    public int getResultCode() {
-        return mResultCode;
-    }
-
-    /**
-     * Returns the error message associated with this result.
-     *
-     * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
-     */
-    @Nullable
-    public String getErrorMessage() {
-        return mErrorMessage;
-    }
-
     @Override
     public int describeContents() {
         return 0;
@@ -359,43 +135,5 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         mResultDocumentWrapper.writeToParcel(dest, flags);
         dest.writeBundle(mExtras);
-        dest.writeInt(mResultCode);
-        dest.writeString8(mErrorMessage);
     }
-
-    /**
-     * Result codes.
-     *
-     * @hide
-     */
-    @IntDef(
-            prefix = {"RESULT_"},
-            value = {
-                RESULT_OK,
-                RESULT_DENIED,
-                RESULT_APP_UNKNOWN_ERROR,
-                RESULT_FUNCTION_NOT_FOUND,
-                RESULT_SYSTEM_ERROR,
-                RESULT_INVALID_ARGUMENT,
-                RESULT_DISABLED,
-                RESULT_CANCELLED
-            })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ResultCode {}
-
-    /**
-     * Error categories.
-     *
-     * @hide
-     */
-    @IntDef(
-            prefix = {"ERROR_CATEGORY_"},
-            value = {
-                ERROR_CATEGORY_UNKNOWN,
-                ERROR_CATEGORY_REQUEST_ERROR,
-                ERROR_CATEGORY_APP,
-                ERROR_CATEGORY_SYSTEM
-            })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ErrorCategory {}
 }
diff --git a/core/java/android/app/appfunctions/GenericDocumentWrapper.java b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
index b29b64e..541ca74 100644
--- a/core/java/android/app/appfunctions/GenericDocumentWrapper.java
+++ b/core/java/android/app/appfunctions/GenericDocumentWrapper.java
@@ -34,9 +34,9 @@
  * <p>{#link {@link Parcel#writeBlob(byte[])}} could take care of whether to pass data via binder
  * directly or Android shared memory if the data is large.
  *
- * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled
- * from the underlying `Parcel` when {@link #getValue()} is called. This optimization
- * allows the system server to pass through the generic document, without unparcel and parcel it.
+ * <p>This class performs lazy unparcelling. The `GenericDocument` is only unparcelled from the
+ * underlying `Parcel` when {@link #getValue()} is called. This optimization allows the system
+ * server to pass through the generic document, without unparcel and parcel it.
  *
  * @hide
  * @see Parcel#writeBlob(byte[])
@@ -45,8 +45,11 @@
     @Nullable
     @GuardedBy("mLock")
     private GenericDocument mGenericDocument;
+
     @GuardedBy("mLock")
-    @Nullable private Parcel mParcel;
+    @Nullable
+    private Parcel mParcel;
+
     private final Object mLock = new Object();
 
     public static final Creator<GenericDocumentWrapper> CREATOR =
diff --git a/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl
index 5323f9b..69bbc0e 100644
--- a/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl
+++ b/core/java/android/app/appfunctions/IExecuteAppFunctionCallback.aidl
@@ -17,8 +17,10 @@
 package android.app.appfunctions;
 
 import android.app.appfunctions.ExecuteAppFunctionResponse;
+import android.app.appfunctions.AppFunctionException;
 
 /** {@hide} */
 oneway interface IExecuteAppFunctionCallback {
-    void onResult(in ExecuteAppFunctionResponse result);
+    void onSuccess(in ExecuteAppFunctionResponse result);
+    void onError(in AppFunctionException exception);
 }
diff --git a/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java
index 0018244..2426daf 100644
--- a/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java
+++ b/core/java/android/app/appfunctions/SafeOneTimeExecuteAppFunctionCallback.java
@@ -17,17 +17,16 @@
 package android.app.appfunctions;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.RemoteException;
 import android.util.Log;
 
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
 
 /**
  * A wrapper of IExecuteAppFunctionCallback which swallows the {@link RemoteException}. This
- * callback is intended for one-time use only. Subsequent calls to onResult() will be ignored.
+ * callback is intended for one-time use only. Subsequent calls to onResult() or onError() will be
+ * ignored.
  *
  * @hide
  */
@@ -38,44 +37,41 @@
 
     @NonNull private final IExecuteAppFunctionCallback mCallback;
 
-    @Nullable private final Consumer<ExecuteAppFunctionResponse> mOnDispatchCallback;
-
     public SafeOneTimeExecuteAppFunctionCallback(@NonNull IExecuteAppFunctionCallback callback) {
-        this(callback, /* onDispatchCallback= */ null);
-    }
-
-    /**
-     * @param callback The callback to wrap.
-     * @param onDispatchCallback An optional callback invoked after the wrapped callback has been
-     *     dispatched with a result. This callback receives the result that has been dispatched.
-     */
-    public SafeOneTimeExecuteAppFunctionCallback(
-            @NonNull IExecuteAppFunctionCallback callback,
-            @Nullable Consumer<ExecuteAppFunctionResponse> onDispatchCallback) {
         mCallback = Objects.requireNonNull(callback);
-        mOnDispatchCallback = onDispatchCallback;
     }
 
     /** Invoke wrapped callback with the result. */
     public void onResult(@NonNull ExecuteAppFunctionResponse result) {
         if (!mOnResultCalled.compareAndSet(false, true)) {
-            Log.w(TAG, "Ignore subsequent calls to onResult()");
+            Log.w(TAG, "Ignore subsequent calls to onResult/onError()");
             return;
         }
         try {
-            mCallback.onResult(result);
+            mCallback.onSuccess(result);
         } catch (RemoteException ex) {
             // Failed to notify the other end. Ignore.
             Log.w(TAG, "Failed to invoke the callback", ex);
         }
-        if (mOnDispatchCallback != null) {
-            mOnDispatchCallback.accept(result);
+    }
+
+    /** Invoke wrapped callback with the error. */
+    public void onError(@NonNull AppFunctionException error) {
+        if (!mOnResultCalled.compareAndSet(false, true)) {
+            Log.w(TAG, "Ignore subsequent calls to onResult/onError()");
+            return;
+        }
+        try {
+            mCallback.onError(error);
+        } catch (RemoteException ex) {
+            // Failed to notify the other end. Ignore.
+            Log.w(TAG, "Failed to invoke the callback", ex);
         }
     }
 
     /**
-     * Disables this callback. Subsequent calls to {@link #onResult(ExecuteAppFunctionResponse)}
-     * will be ignored.
+     * Disables this callback. Subsequent calls to {@link #onResult(ExecuteAppFunctionResponse)} or
+     * {@link #onError(AppFunctionException)} will be ignored.
      */
     public void disable() {
         mOnResultCalled.set(true);
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index a488689..43a46ba 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -1,5 +1,6 @@
 package android.app.assist;
 
+import android.annotation.FlaggedApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
 import android.content.Intent;
@@ -15,6 +16,20 @@
  * {@link android.app.Activity#onProvideAssistContent Activity.onProvideAssistContent}.
  */
 public class AssistContent implements Parcelable {
+    /**
+     * Extra for a {@link Bundle} that provides contextual AppFunction's information about the
+     * content currently being viewed in the application.
+     * <p>
+     * This extra can be optionally supplied in the {@link AssistContent#getExtras()} bundle.
+     * <p>
+     * The schema of the {@link Bundle} in this extra is defined in the AppFunction SDK.
+     *
+     * @see android.app.appfunctions.AppFunctionManager
+     */
+    @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER)
+    public static final String EXTRA_APP_FUNCTION_DATA =
+            "android.app.assist.extra.APP_FUNCTION_DATA";
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private boolean mIsAppProvidedIntent = false;
     private boolean mIsAppProvidedWebUri = false;
diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig
index 9a64519..c8455c1 100644
--- a/core/java/android/app/multitasking.aconfig
+++ b/core/java/android/app/multitasking.aconfig
@@ -8,3 +8,11 @@
     description: "Enables PiP UI state callback on entering"
     bug: "303718131"
 }
+
+flag {
+    name: "enable_tv_implicit_enter_pip_restriction"
+    is_exported: true
+    namespace: "tv_system_ui"
+    description: "Enables restrictions to PiP entry on TV for setAutoEnterEnabled and lifecycle methods"
+    bug: "283115999"
+}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 0fc4291..ee93870 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -8,8 +8,7 @@
 flag {
   name: "notifications_redesign_app_icons"
   namespace: "systemui"
-  description: "Notifications Redesign: Use app icons in notification rows (not to be confused with"
-    " notifications_use_app_icons, notifications_use_app_icon_in_row which are just experiments)."
+  description: "Notifications Redesign: Use app icons in notification rows"
   bug: "371174789"
 }
 
@@ -110,31 +109,6 @@
   }
 }
 
-# vvv Prototypes for using app icons in notifications vvv
-
-flag {
-  name: "notifications_use_app_icon"
-  namespace: "systemui"
-  description: "Experiment to replace the small icon in a notification with the app icon. This includes the status bar, AOD, shelf and notification row itself."
-  bug: "335211019"
-}
-
-flag {
-  name: "notifications_use_app_icon_in_row"
-  namespace: "systemui"
-  description: "Experiment to replace the small icon in a notification row with the app icon."
-  bug: "335211019"
-}
-
-flag {
-  name: "notifications_use_monochrome_app_icon"
-  namespace: "systemui"
-  description: "Experiment to replace the notification icon in the status bar and shelf with the monochrome app icon, if available."
-  bug: "335211019"
-}
-
-# ^^^ Prototypes for using app icons in notifications ^^^
-
 flag {
   name: "notification_expansion_optional"
   namespace: "systemui"
diff --git a/core/java/android/app/performance.aconfig b/core/java/android/app/performance.aconfig
index f51f748..61b53f9 100644
--- a/core/java/android/app/performance.aconfig
+++ b/core/java/android/app/performance.aconfig
@@ -18,3 +18,20 @@
      description: "Enforce PropertyInvalidatedCache.setTestMode() protocol"
      bug: "360897450"
 }
+
+flag {
+     namespace: "system_performance"
+     name: "pic_isolate_cache_by_uid"
+     is_fixed_read_only: true
+     description: "Ensure that different UIDs use different caches"
+     bug: "373752556"
+}
+
+flag {
+     namespace: "system_performance"
+     name: "pic_isolated_cache_statistics"
+     is_fixed_read_only: true
+     description: "Collects statistics for cache UID isolation strategies"
+     bug: "373752556"
+}
+
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 186f7b3..6086f24 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6802,6 +6802,12 @@
     public static final String MEDIA_QUALITY_SERVICE = "media_quality";
 
     /**
+     * Service to perform operations needed for dynamic instrumentation.
+     * @hide
+     */
+    public static final String DYNAMIC_INSTRUMENTATION_SERVICE = "dynamic_instrumentation";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index 26ecbd1..f23c193 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -95,3 +95,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "layout_readwrite_flags"
+    is_exported: true
+    namespace: "resource_manager"
+    description: "Feature flag for allowing read/write flags in layout files"
+    bug: "377974898"
+    # This flag is used to control aapt2 behavior.
+    is_fixed_read_only: true
+}
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index ef59e0a..93ef5c3 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -45,7 +45,7 @@
  * </p>
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("CursorWindow_host")
+@RavenwoodRedirectionClass("CursorWindow_ravenwood")
 public class CursorWindow extends SQLiteClosable implements Parcelable {
     private static final String STATS_TAG = "CursorWindowStats";
 
diff --git a/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java b/core/java/android/database/CursorWindow_ravenwood.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
rename to core/java/android/database/CursorWindow_ravenwood.java
index e21a9cd..990ec5e 100644
--- a/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
+++ b/core/java/android/database/CursorWindow_ravenwood.java
@@ -17,6 +17,7 @@
 
 import android.database.sqlite.SQLiteException;
 import android.os.Parcel;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 import android.util.Base64;
 
 import java.text.DecimalFormat;
@@ -26,9 +27,10 @@
 import java.util.HashMap;
 import java.util.List;
 
-public class CursorWindow_host {
+@RavenwoodKeepWholeClass
+class CursorWindow_ravenwood {
 
-    private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>();
+    private static final HashMap<Long, CursorWindow_ravenwood> sInstances = new HashMap<>();
     private static long sNextId = 1;
 
     private String mName;
@@ -41,7 +43,7 @@
     private final List<Row> mRows = new ArrayList<>();
 
     public static long nativeCreate(String name, int cursorWindowSize) {
-        CursorWindow_host instance = new CursorWindow_host();
+        CursorWindow_ravenwood instance = new CursorWindow_ravenwood();
         instance.mName = name;
         long instanceId = sNextId++;
         sInstances.put(instanceId, instance);
@@ -66,7 +68,7 @@
     }
 
     public static boolean nativeAllocRow(long windowPtr) {
-        CursorWindow_host instance = sInstances.get(windowPtr);
+        CursorWindow_ravenwood instance = sInstances.get(windowPtr);
         Row row = new Row();
         row.mFields = new String[instance.mColumnNum];
         row.mTypes = new int[instance.mColumnNum];
@@ -76,7 +78,7 @@
     }
 
     private static boolean put(long windowPtr, String value, int type, int row, int column) {
-        CursorWindow_host instance = sInstances.get(windowPtr);
+        CursorWindow_ravenwood instance = sInstances.get(windowPtr);
         if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
             return false;
         }
@@ -87,7 +89,7 @@
     }
 
     public static int nativeGetType(long windowPtr, int row, int column) {
-        CursorWindow_host instance = sInstances.get(windowPtr);
+        CursorWindow_ravenwood instance = sInstances.get(windowPtr);
         if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
             return Cursor.FIELD_TYPE_NULL;
         }
@@ -101,7 +103,7 @@
     }
 
     public static String nativeGetString(long windowPtr, int row, int column) {
-        CursorWindow_host instance = sInstances.get(windowPtr);
+        CursorWindow_ravenwood instance = sInstances.get(windowPtr);
         if (row >= instance.mRows.size() || column >= instance.mColumnNum) {
             return null;
         }
@@ -164,7 +166,7 @@
     }
 
     public static void nativeWriteToParcel(long windowPtr, Parcel parcel) {
-        CursorWindow_host window = sInstances.get(windowPtr);
+        CursorWindow_ravenwood window = sInstances.get(windowPtr);
         parcel.writeString(window.mName);
         parcel.writeInt(window.mColumnNum);
         parcel.writeInt(window.mRows.size());
@@ -176,7 +178,7 @@
 
     public static long nativeCreateFromParcel(Parcel parcel) {
         long windowPtr = nativeCreate(null, 0);
-        CursorWindow_host window = sInstances.get(windowPtr);
+        CursorWindow_ravenwood window = sInstances.get(windowPtr);
         window.mName = parcel.readString();
         window.mColumnNum = parcel.readInt();
         int rowCount = parcel.readInt();
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index b785630..75e2058 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -994,7 +994,7 @@
         AttributionSourceState contextAttributionSourceState =
                 contextAttributionSource.asState();
 
-        if (Flags.useContextAttributionSource() && useContextAttributionSource) {
+        if (Flags.dataDeliveryPermissionChecks() && useContextAttributionSource) {
             return contextAttributionSourceState;
         } else {
             AttributionSourceState clientAttribution =
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 86bbd4a..2f6c6a3 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -3371,6 +3371,32 @@
     public static final int CONTROL_AUTOFRAMING_AUTO = 2;
 
     //
+    // Enumeration values for CaptureRequest#CONTROL_ZOOM_METHOD
+    //
+
+    /**
+     * <p>The camera device automatically detects whether the application does zoom with
+     * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and in turn decides which
+     * metadata tag reflects the effective zoom level.</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see CaptureRequest#CONTROL_ZOOM_METHOD
+     */
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final int CONTROL_ZOOM_METHOD_AUTO = 0;
+
+    /**
+     * <p>The application intends to control zoom via {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and
+     * the effective zoom level is reflected by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results.</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CaptureRequest#CONTROL_ZOOM_METHOD
+     */
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1;
+
+    //
     // Enumeration values for CaptureRequest#EDGE_MODE
     //
 
@@ -4289,6 +4315,39 @@
      */
     public static final int SYNC_FRAME_NUMBER_UNKNOWN = -2;
 
+    //
+    // Enumeration values for CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR
+    //
+
+    /**
+     * <p>The camera can't accurately assess the scene's lighting to determine if a Night Mode
+     * Camera Extension capture would improve the photo. This can happen when the current
+     * camera configuration doesn't support night mode indicator detection, such as when
+     * the auto exposure mode is ON_AUTO_FLASH, ON_ALWAYS_FLASH, ON_AUTO_FLASH_REDEYE, or
+     * ON_EXTERNAL_FLASH.</p>
+     * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR
+     */
+    @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR)
+    public static final int EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN = 0;
+
+    /**
+     * <p>The camera has detected lighting conditions that are sufficiently bright. Night
+     * Mode Camera Extensions is available but may not be able to optimize the camera
+     * settings to take a higher quality photo.</p>
+     * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR
+     */
+    @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR)
+    public static final int EXTENSION_NIGHT_MODE_INDICATOR_OFF = 1;
+
+    /**
+     * <p>The camera has detected low-light conditions. It is recommended to use Night Mode
+     * Camera Extension to optimize the camera settings to take a high-quality photo in
+     * the dark.</p>
+     * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR
+     */
+    @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR)
+    public static final int EXTENSION_NIGHT_MODE_INDICATOR_ON = 2;
+
     /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
      * End generated code
      *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 8142bbe..9846cac 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -21,7 +21,6 @@
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.impl.ExtensionKey;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
 import android.hardware.camera2.params.OutputConfiguration;
@@ -2668,6 +2667,45 @@
             new Key<Integer>("android.control.autoframing", int.class);
 
     /**
+     * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}
+     * to control zoom levels.</p>
+     * <p>If set to AUTO, the camera device detects which capture request key the application uses
+     * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If
+     * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture
+     * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture
+     * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective
+     * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value
+     * for this control, and also the behavior of the OS before Android version
+     * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p>
+     * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled
+     * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in
+     * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO,
+     * with which the camera device cannot know if the application is using cropRegion or
+     * zoomRatio at 1.0x.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_ZOOM_METHOD_AUTO
+     * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final Key<Integer> CONTROL_ZOOM_METHOD =
+            new Key<Integer>("android.control.zoomMethod", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index ae72ca4..674fc66 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -22,7 +22,6 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.CaptureResultExtras;
-import android.hardware.camera2.impl.ExtensionKey;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
 import android.hardware.camera2.utils.TypeReference;
@@ -2915,6 +2914,45 @@
             new Key<Integer>("android.control.lowLightBoostState", int.class);
 
     /**
+     * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}
+     * to control zoom levels.</p>
+     * <p>If set to AUTO, the camera device detects which capture request key the application uses
+     * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If
+     * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture
+     * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture
+     * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective
+     * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value
+     * for this control, and also the behavior of the OS before Android version
+     * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p>
+     * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled
+     * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in
+     * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO,
+     * with which the camera device cannot know if the application is using cropRegion or
+     * zoomRatio at 1.0x.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_ZOOM_METHOD_AUTO
+     * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final Key<Integer> CONTROL_ZOOM_METHOD =
+            new Key<Integer>("android.control.zoomMethod", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
@@ -6016,6 +6054,38 @@
     public static final Key<Integer> EXTENSION_STRENGTH =
             new Key<Integer>("android.extension.strength", int.class);
 
+    /**
+     * <p>Indicates when to activate Night Mode Camera Extension for high-quality
+     * still captures in low-light conditions.</p>
+     * <p>Provides awareness to the application when the current scene can benefit from using a
+     * Night Mode Camera Extension to take a high-quality photo.</p>
+     * <p>Support for this capture result can be queried via
+     * {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p>
+     * <p>If the device supports this capability then it will also support
+     * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_NIGHT NIGHT}
+     * and will be available in both
+     * {@link android.hardware.camera2.CameraCaptureSession sessions} and
+     * {@link android.hardware.camera2.CameraExtensionSession sessions}.</p>
+     * <p>The value will be {@code UNKNOWN} in the following auto exposure modes: ON_AUTO_FLASH,
+     * ON_ALWAYS_FLASH, ON_AUTO_FLASH_REDEYE, or ON_EXTERNAL_FLASH.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN UNKNOWN}</li>
+     *   <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_OFF OFF}</li>
+     *   <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_ON ON}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @see #EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN
+     * @see #EXTENSION_NIGHT_MODE_INDICATOR_OFF
+     * @see #EXTENSION_NIGHT_MODE_INDICATOR_ON
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR)
+    public static final Key<Integer> EXTENSION_NIGHT_MODE_INDICATOR =
+            new Key<Integer>("android.extension.nightModeIndicator", int.class);
+
     /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
      * End generated code
      *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index e22c263..1cc0856 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -437,7 +437,7 @@
     }
 
     @Override
-    public void writeToParcel(Parcel dest, int flags) {
+    public synchronized void writeToParcel(Parcel dest, int flags) {
         nativeWriteToParcel(dest, mMetadataPtr);
     }
 
@@ -479,7 +479,7 @@
         return getBase(key);
     }
 
-    public void readFromParcel(Parcel in) {
+    public synchronized void readFromParcel(Parcel in) {
         nativeReadFromParcel(in, mMetadataPtr);
         updateNativeAllocation();
     }
@@ -592,28 +592,33 @@
     }
 
     private <T> T getBase(Key<T> key) {
-        int tag;
-        if (key.hasTag()) {
-            tag = key.getTag();
-        } else {
-            tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName());
-            key.cacheTag(tag);
-        }
-        byte[] values = readValues(tag);
-        if (values == null) {
-            // If the key returns null, use the fallback key if exists.
-            // This is to support old key names for the newly published keys.
-            if (key.mFallbackName == null) {
-                return null;
+        int tag, nativeType;
+        byte[] values = null;
+        synchronized (this) {
+            if (key.hasTag()) {
+                tag = key.getTag();
+            } else {
+                tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.getName());
+                key.cacheTag(tag);
             }
-            tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.mFallbackName);
             values = readValues(tag);
             if (values == null) {
-                return null;
+                // If the key returns null, use the fallback key if exists.
+                // This is to support old key names for the newly published keys.
+                if (key.mFallbackName == null) {
+                    return null;
+                }
+                tag = nativeGetTagFromKeyLocal(mMetadataPtr, key.mFallbackName);
+                values = readValues(tag);
+                if (values == null) {
+                    return null;
+                }
             }
-        }
 
-        int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
+            nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
+        }
+        // This block of code doesn't need to be synchronized since we aren't writing or reading
+        // from the metadata buffer for this instance of CameraMetadataNative.
         Marshaler<T> marshaler = getMarshalerForKey(key, nativeType);
         ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder());
         return marshaler.unmarshal(buffer);
@@ -1945,8 +1950,12 @@
         setBase(key.getNativeKey(), value);
     }
 
-    private <T> void setBase(Key<T> key, T value) {
-        int tag;
+    // The whole method needs to be synchronized since we're making
+    // multiple calls to the native layer. From one call to the other (within setBase)
+    // we expect the metadata's properties such as vendor id etc to
+    // stay the same and as a result the whole method should be synchronized for safety.
+    private synchronized <T> void setBase(Key<T> key, T value) {
+        int tag, nativeType;
         if (key.hasTag()) {
             tag = key.getTag();
         } else {
@@ -1959,7 +1968,7 @@
             return;
         } // else update the entry to a new value
 
-        int nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
+        nativeType = nativeGetTypeFromTagLocal(mMetadataPtr, tag);
         Marshaler<T> marshaler = getMarshalerForKey(key, nativeType);
         int size = marshaler.calculateMarshalSize(value);
 
@@ -2162,7 +2171,7 @@
         return true;
     }
 
-    private void updateNativeAllocation() {
+    private synchronized void updateNativeAllocation() {
         long currentBufferSize = nativeGetBufferSize(mMetadataPtr);
 
         if (currentBufferSize != mBufferSize) {
@@ -2245,6 +2254,11 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private long mMetadataPtr; // native std::shared_ptr<CameraMetadata>*
 
+    // FastNative doesn't work with synchronized methods and we can do synchronization
+    // wherever needed in the java layer (caller). At some places in java such as
+    // setBase() / getBase(), we do need to synchronize the whole method, so leaving
+    // synchronized out for these native methods.
+
     @FastNative
     private static native long nativeAllocate();
     @FastNative
@@ -2254,28 +2268,41 @@
 
     @FastNative
     private static native void nativeUpdate(long dst, long src);
-    private static synchronized native void nativeWriteToParcel(Parcel dest, long ptr);
-    private static synchronized native void nativeReadFromParcel(Parcel source, long ptr);
-    private static synchronized native void nativeSwap(long ptr, long otherPtr)
+    @FastNative
+    private static native void nativeWriteToParcel(Parcel dest, long ptr);
+    @FastNative
+    private static native void nativeReadFromParcel(Parcel source, long ptr);
+    @FastNative
+    private static native void nativeSwap(long ptr, long otherPtr)
             throws NullPointerException;
     @FastNative
     private static native void nativeSetVendorId(long ptr, long vendorId);
-    private static synchronized native void nativeClose(long ptr);
-    private static synchronized native boolean nativeIsEmpty(long ptr);
-    private static synchronized native int nativeGetEntryCount(long ptr);
-    private static synchronized native long nativeGetBufferSize(long ptr);
+    @FastNative
+    private static native void nativeClose(long ptr);
+    @FastNative
+    private static native boolean nativeIsEmpty(long ptr);
+    @FastNative
+    private static native int nativeGetEntryCount(long ptr);
+    @FastNative
+    private static native long nativeGetBufferSize(long ptr);
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    private static synchronized native byte[] nativeReadValues(int tag, long ptr);
-    private static synchronized native void nativeWriteValues(int tag, byte[] src, long ptr);
-    private static synchronized native void nativeDump(long ptr) throws IOException; // dump to LOGD
+    @FastNative
+    private static native byte[] nativeReadValues(int tag, long ptr);
+    @FastNative
+    private static native void nativeWriteValues(int tag, byte[] src, long ptr);
+    @FastNative
+    private static native void nativeDump(long ptr) throws IOException; // dump to LOGD
 
-    private static synchronized native ArrayList nativeGetAllVendorKeys(long ptr, Class keyClass);
+    @FastNative
+    private static native ArrayList nativeGetAllVendorKeys(long ptr, Class keyClass);
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    private static synchronized native int nativeGetTagFromKeyLocal(long ptr, String keyName)
+    @FastNative
+    private static native int nativeGetTagFromKeyLocal(long ptr, String keyName)
             throws IllegalArgumentException;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    private static synchronized native int nativeGetTypeFromTagLocal(long ptr, int tag)
+    @FastNative
+    private static native int nativeGetTypeFromTagLocal(long ptr, int tag)
             throws IllegalArgumentException;
     @FastNative
     private static native int nativeGetTagFromKey(String keyName, long vendorId)
@@ -2293,7 +2320,7 @@
      * @throws NullPointerException if other was null
      * @hide
      */
-    public void swap(CameraMetadataNative other) {
+    public synchronized void swap(CameraMetadataNative other) {
         nativeSwap(mMetadataPtr, other.mMetadataPtr);
         mCameraId = other.mCameraId;
         mHasMandatoryConcurrentStreams = other.mHasMandatoryConcurrentStreams;
@@ -2308,14 +2335,14 @@
      *
      * @hide
      */
-    public void setVendorId(long vendorId) {
+    public synchronized void setVendorId(long vendorId) {
         nativeSetVendorId(mMetadataPtr, vendorId);
     }
 
     /**
      * @hide
      */
-    public int getEntryCount() {
+    public synchronized int getEntryCount() {
         return nativeGetEntryCount(mMetadataPtr);
     }
 
@@ -2324,7 +2351,7 @@
      *
      * @hide
      */
-    public boolean isEmpty() {
+    public synchronized boolean isEmpty() {
         return nativeIsEmpty(mMetadataPtr);
     }
 
@@ -2343,7 +2370,7 @@
      *
      * @hide
      */
-    public <K>  ArrayList<K> getAllVendorKeys(Class<K> keyClass) {
+    public synchronized <K> ArrayList<K> getAllVendorKeys(Class<K> keyClass) {
         if (keyClass == null) {
             throw new NullPointerException();
         }
@@ -2398,7 +2425,7 @@
      *
      * @hide
      */
-    public void writeValues(int tag, byte[] src) {
+    public synchronized void writeValues(int tag, byte[] src) {
         nativeWriteValues(tag, src, mMetadataPtr);
     }
 
@@ -2413,7 +2440,7 @@
      * @return {@code null} if there were 0 entries for this tag, a byte[] otherwise.
      * @hide
      */
-    public byte[] readValues(int tag) {
+    public synchronized byte[] readValues(int tag) {
         // TODO: Optimization. Native code returns a ByteBuffer instead.
         return nativeReadValues(tag, mMetadataPtr);
     }
@@ -2426,7 +2453,7 @@
      *
      * @hide
      */
-    public void dumpToLog() {
+    public synchronized void dumpToLog() {
         try {
             nativeDump(mMetadataPtr);
         } catch (IOException e) {
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index a452226..28da644 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -16,6 +16,7 @@
 
 package android.hardware.display;
 
+import static android.Manifest.permission.MANAGE_DISPLAYS;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.HdrCapabilities.HdrType;
 import static android.view.Display.INVALID_DISPLAY;
@@ -1764,6 +1765,29 @@
     }
 
     /**
+     * @return The current display topology that represents the relative positions of extended
+     * displays.
+     *
+     * @hide
+     */
+    @RequiresPermission(MANAGE_DISPLAYS)
+    @Nullable
+    public DisplayTopology getDisplayTopology() {
+        return mGlobal.getDisplayTopology();
+    }
+
+    /**
+     * Set the relative positions between extended displays (display topology).
+     * @param topology The display topology to be set
+     *
+     * @hide
+     */
+    @RequiresPermission(MANAGE_DISPLAYS)
+    public void setDisplayTopology(DisplayTopology topology) {
+        mGlobal.setDisplayTopology(topology);
+    }
+
+    /**
      * Listens for changes in available display devices.
      */
     public interface DisplayListener {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 644850a..03b44f6 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -18,6 +18,7 @@
 
 
 import static android.hardware.display.DisplayManager.EventFlag;
+import static android.Manifest.permission.MANAGE_DISPLAYS;
 import static android.view.Display.HdrCapabilities.HdrType;
 
 import android.Manifest;
@@ -1285,6 +1286,31 @@
         }
     }
 
+    /**
+     * @see DisplayManager#getDisplayTopology
+     */
+    @RequiresPermission(MANAGE_DISPLAYS)
+    @Nullable
+    public DisplayTopology getDisplayTopology() {
+        try {
+            return mDm.getDisplayTopology();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @see DisplayManager#setDisplayTopology
+     */
+    @RequiresPermission(MANAGE_DISPLAYS)
+    public void setDisplayTopology(DisplayTopology topology) {
+        try {
+            mDm.setDisplayTopology(topology);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
         @Override
         public void onDisplayEvent(int displayId, @DisplayEvent int event) {
diff --git a/core/java/android/hardware/display/DisplayTopology.aidl b/core/java/android/hardware/display/DisplayTopology.aidl
new file mode 100644
index 0000000..e69b777
--- /dev/null
+++ b/core/java/android/hardware/display/DisplayTopology.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.hardware.display;
+
+parcelable DisplayTopology;
diff --git a/services/core/java/com/android/server/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java
similarity index 73%
rename from services/core/java/com/android/server/display/DisplayTopology.java
rename to core/java/android/hardware/display/DisplayTopology.java
index fdadafe..e349b81 100644
--- a/services/core/java/com/android/server/display/DisplayTopology.java
+++ b/core/java/android/hardware/display/DisplayTopology.java
@@ -14,25 +14,34 @@
  * limitations under the License.
  */
 
-package com.android.server.display;
+package android.hardware.display;
 
-import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_BOTTOM;
-import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_LEFT;
-import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_TOP;
-import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_RIGHT;
+import static android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM;
+import static android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT;
+import static android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT;
+import static android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.graphics.RectF;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
 import android.util.Slog;
 import android.view.Display;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
@@ -42,24 +51,59 @@
 /**
  * Represents the relative placement of extended displays.
  * Does not support concurrent calls, so a lock should be held when calling into this class.
+ *
+ * @hide
  */
-class DisplayTopology {
+public final class DisplayTopology implements Parcelable {
     private static final String TAG = "DisplayTopology";
     private static final float EPSILON = 0.0001f;
 
+    @android.annotation.NonNull
+    public static final Creator<DisplayTopology> CREATOR =
+            new Creator<>() {
+                @Override
+                public DisplayTopology createFromParcel(Parcel source) {
+                    return new DisplayTopology(source);
+                }
+
+                @Override
+                public DisplayTopology[] newArray(int size) {
+                    return new DisplayTopology[size];
+                }
+            };
+
     /**
      * The topology tree
      */
     @Nullable
-    @VisibleForTesting
-    TreeNode mRoot;
+    private TreeNode mRoot;
 
     /**
      * The logical display ID of the primary display that will show certain UI elements.
      * This is not necessarily the same as the default display.
      */
+    private int mPrimaryDisplayId = Display.INVALID_DISPLAY;
+
+    public DisplayTopology() {}
+
     @VisibleForTesting
-    int mPrimaryDisplayId = Display.INVALID_DISPLAY;
+    public DisplayTopology(TreeNode root, int primaryDisplayId) {
+        mRoot = root;
+        mPrimaryDisplayId = primaryDisplayId;
+    }
+
+    public DisplayTopology(Parcel source) {
+        this(source.readTypedObject(TreeNode.CREATOR), source.readInt());
+    }
+
+    @Nullable
+    public TreeNode getRoot() {
+        return mRoot;
+    }
+
+    public int getPrimaryDisplayId() {
+        return mPrimaryDisplayId;
+    }
 
     /**
      * Add a display to the topology.
@@ -69,7 +113,7 @@
      * @param width The width of the display
      * @param height The height of the display
      */
-    void addDisplay(int displayId, float width, float height) {
+    public void addDisplay(int displayId, float width, float height) {
         addDisplay(displayId, width, height, /* shouldLog= */ true);
     }
 
@@ -79,7 +123,7 @@
      * one by one.
      * @param displayId The logical display ID
      */
-    void removeDisplay(int displayId) {
+    public void removeDisplay(int displayId) {
         if (findDisplay(displayId, mRoot) == null) {
             return;
         }
@@ -106,11 +150,22 @@
         }
     }
 
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedObject(mRoot, flags);
+        dest.writeInt(mPrimaryDisplayId);
+    }
+
     /**
      * Print the object's state and debug information into the given stream.
      * @param pw The stream to dump information to.
      */
-    void dump(PrintWriter pw) {
+    public void dump(PrintWriter pw) {
         pw.println("DisplayTopology:");
         pw.println("--------------------");
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
@@ -126,13 +181,21 @@
         }
     }
 
+    @Override
+    public String toString() {
+        StringWriter out = new StringWriter();
+        PrintWriter writer = new PrintWriter(out);
+        dump(writer);
+        return out.toString();
+    }
+
     private void addDisplay(int displayId, float width, float height, boolean shouldLog) {
         if (findDisplay(displayId, mRoot) != null) {
             throw new IllegalArgumentException(
                     "DisplayTopology: attempting to add a display that already exists");
         }
         if (mRoot == null) {
-            mRoot = new TreeNode(displayId, width, height, /* position= */ null, /* offset= */ 0);
+            mRoot = new TreeNode(displayId, width, height, /* position= */ 0, /* offset= */ 0);
             mPrimaryDisplayId = displayId;
             if (shouldLog) {
                 Slog.i(TAG, "First display added: " + mRoot);
@@ -241,7 +304,7 @@
      * Update the topology to remove any overlaps between displays.
      */
     @VisibleForTesting
-    void normalize() {
+    public void normalize() {
         if (mRoot == null) {
             return;
         }
@@ -341,6 +404,8 @@
                 case POSITION_RIGHT -> floatEquals(parentBounds.right, childBounds.left);
                 case POSITION_TOP -> floatEquals(parentBounds.top, childBounds.bottom);
                 case POSITION_BOTTOM -> floatEquals(parentBounds.bottom, childBounds.top);
+                default -> throw new IllegalStateException(
+                        "Unexpected value: " + targetDisplay.mPosition);
             };
             // Check that the offset is within bounds
             areTouching &= switch (targetDisplay.mPosition) {
@@ -350,6 +415,8 @@
                 case POSITION_TOP, POSITION_BOTTOM ->
                         childBounds.right + EPSILON >= parentBounds.left
                                 && childBounds.left <= parentBounds.right + EPSILON;
+                default -> throw new IllegalStateException(
+                        "Unexpected value: " + targetDisplay.mPosition);
             };
 
             if (!areTouching) {
@@ -379,36 +446,56 @@
      * @param b second float to compare
      * @return whether the two values are within a small enough tolerance value
      */
-    public static boolean floatEquals(float a, float b) {
-        return a == b || Float.isNaN(a) && Float.isNaN(b) || Math.abs(a - b) < EPSILON;
+    private static boolean floatEquals(float a, float b) {
+        return a == b || (Float.isNaN(a) && Float.isNaN(b)) || Math.abs(a - b) < EPSILON;
     }
 
-    @VisibleForTesting
-    static class TreeNode {
+    public static final class TreeNode implements Parcelable {
+        public static final int POSITION_LEFT = 0;
+        public static final int POSITION_TOP = 1;
+        public static final int POSITION_RIGHT = 2;
+        public static final int POSITION_BOTTOM = 3;
+
+        @IntDef(prefix = { "POSITION_" }, value = {
+                POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface Position{}
+
+        @android.annotation.NonNull
+        public static final Creator<TreeNode> CREATOR =
+                new Creator<>() {
+                    @Override
+                    public TreeNode createFromParcel(Parcel source) {
+                        return new TreeNode(source);
+                    }
+
+                    @Override
+                    public TreeNode[] newArray(int size) {
+                        return new TreeNode[size];
+                    }
+                };
 
         /**
          * The logical display ID
          */
-        @VisibleForTesting
-        final int mDisplayId;
+        private final int mDisplayId;
 
         /**
          * The width of the display in density-independent pixels (dp).
          */
-        @VisibleForTesting
-        float mWidth;
+        private final float mWidth;
 
         /**
          * The height of the display in density-independent pixels (dp).
          */
-        @VisibleForTesting
-        float mHeight;
+        private final float mHeight;
 
         /**
          * The position of this display relative to its parent.
          */
-        @VisibleForTesting
-        Position mPosition;
+        @Position
+        private int mPosition;
 
         /**
          * The distance from the top edge of the parent display to the top edge of this display (in
@@ -416,13 +503,13 @@
          * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit
          * used is density-independent pixels (dp).
          */
-        @VisibleForTesting
-        float mOffset;
+        private float mOffset;
+
+        private final List<TreeNode> mChildren = new ArrayList<>();
 
         @VisibleForTesting
-        final List<TreeNode> mChildren = new ArrayList<>();
-
-        TreeNode(int displayId, float width, float height, Position position, float offset) {
+        public TreeNode(int displayId, float width, float height, @Position int position,
+                float offset) {
             mDisplayId = displayId;
             mWidth = width;
             mHeight = height;
@@ -430,11 +517,76 @@
             mOffset = offset;
         }
 
+        public TreeNode(Parcel source) {
+            this(source.readInt(), source.readFloat(), source.readFloat(), source.readInt(),
+                    source.readFloat());
+            source.readTypedList(mChildren, CREATOR);
+        }
+
+        public int getDisplayId() {
+            return mDisplayId;
+        }
+
+        public float getWidth() {
+            return mWidth;
+        }
+
+        public float getHeight() {
+            return mHeight;
+        }
+
+        public int getPosition() {
+            return mPosition;
+        }
+
+        public float getOffset() {
+            return mOffset;
+        }
+
+        public List<TreeNode> getChildren() {
+            return Collections.unmodifiableList(mChildren);
+        }
+
+        @Override
+        public String toString() {
+            return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight
+                    + ", position=" + positionToString(mPosition) + ", offset=" + mOffset + "}";
+        }
+
+        /**
+         * @param position The position
+         * @return The string representation
+         */
+        public static String positionToString(@Position int position) {
+            return switch (position) {
+                case POSITION_LEFT -> "left";
+                case POSITION_TOP -> "top";
+                case POSITION_RIGHT -> "right";
+                case POSITION_BOTTOM -> "bottom";
+                default -> throw new IllegalStateException("Unexpected value: " + position);
+            };
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeInt(mDisplayId);
+            dest.writeFloat(mWidth);
+            dest.writeFloat(mHeight);
+            dest.writeInt(mPosition);
+            dest.writeFloat(mOffset);
+            dest.writeTypedList(mChildren);
+        }
+
         /**
          * Print the object's state and debug information into the given stream.
          * @param ipw The stream to dump information to.
          */
-        void dump(IndentingPrintWriter ipw) {
+        public void dump(IndentingPrintWriter ipw) {
             ipw.println(this);
             ipw.increaseIndent();
             for (TreeNode child : mChildren) {
@@ -443,15 +595,12 @@
             ipw.decreaseIndent();
         }
 
-        @Override
-        public String toString() {
-            return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight
-                    + ", position=" + mPosition + ", offset=" + mOffset + "}";
-        }
-
+        /**
+         * @param child The child to add
+         */
         @VisibleForTesting
-        enum Position {
-            POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
+        public void addChild(TreeNode child) {
+            mChildren.add(child);
         }
     }
 }
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index b612bca..4fbdf7f 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -23,6 +23,7 @@
 import android.hardware.display.BrightnessInfo;
 import android.hardware.display.Curve;
 import android.hardware.graphics.common.DisplayDecorationSupport;
+import android.hardware.display.DisplayTopology;
 import android.hardware.display.HdrConversionMode;
 import android.hardware.display.IDisplayManagerCallback;
 import android.hardware.display.IVirtualDisplayCallback;
@@ -254,4 +255,13 @@
     // Get the default doze brightness
     @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS")
     float getDefaultDozeBrightness(int displayId);
+
+    // Get the display topology
+    @EnforcePermission("MANAGE_DISPLAYS")
+    @nullable
+    DisplayTopology getDisplayTopology();
+
+    // Set the display topology
+    @EnforcePermission("MANAGE_DISPLAYS")
+    void setDisplayTopology(in DisplayTopology topology);
 }
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 9d42b67..24951c4 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -117,6 +117,10 @@
     public static final int KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW = 69;
     public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 70;
     public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71;
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72;
+    public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73;
+    public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74;
+    public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -203,6 +207,10 @@
             KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
             KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
             KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
+            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
+            KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
+            KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+            KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
@@ -773,6 +781,14 @@
                 return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW";
             case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE:
                 return "KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE";
+            case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN";
+            case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
+                return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT";
+            case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
+                return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION";
+            case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
+                return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 38e32c6..a8eb11d 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -30,14 +30,6 @@
 
 flag {
     namespace: "input_native"
-    name: "emoji_and_screenshot_keycodes_available"
-    is_exported: true
-    description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots"
-    bug: "315307777"
-}
-
-flag {
-    namespace: "input_native"
     name: "keyboard_a11y_slow_keys_flag"
     description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user"
     bug: "294546335"
@@ -153,6 +145,13 @@
 }
 
 flag {
+    name: "enable_new_25q2_keycodes"
+    namespace: "input"
+    description: "Enables new 25Q2 keycodes"
+    bug: "365920375"
+}
+
+flag {
   name: "override_power_key_behavior_in_focused_window"
   namespace: "input_native"
   description: "Allows privileged focused windows to capture power key events."
diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java
index 858ec23..af715e4 100644
--- a/core/java/android/hardware/location/ContextHubInfo.java
+++ b/core/java/android/hardware/location/ContextHubInfo.java
@@ -15,7 +15,6 @@
  */
 package android.hardware.location;
 
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 6284e70..494bfc9 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -18,6 +18,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -31,7 +32,6 @@
 import android.app.PendingIntent;
 import android.chre.flags.Flags;
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.contexthub.ErrorCode;
 import android.os.Handler;
@@ -484,15 +484,33 @@
         }
     }
 
-   /**
-    * Helper function to generate a stub for a query transaction callback.
-    *
-    * @param transaction the transaction to unblock when complete
-    *
-    * @return the callback
-    *
-    * @hide
-    */
+    /**
+     * Returns the list of HubInfo objects describing the available hubs (including ContextHub and
+     * VendorHub). This method is primarily used for debugging purposes as most clients care about
+     * endpoints and services more than hubs.
+     *
+     * @return the list of HubInfo objects
+     * @see HubInfo
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @NonNull
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    public List<HubInfo> getHubs() {
+        try {
+            return mService.getHubs();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Helper function to generate a stub for a query transaction callback.
+     *
+     * @param transaction the transaction to unblock when complete
+     * @return the callback
+     * @hide
+     */
     private IContextHubTransactionCallback createQueryCallback(
             ContextHubTransaction<List<NanoAppState>> transaction) {
         return new IContextHubTransactionCallback.Stub() {
diff --git a/core/java/android/hardware/location/HubInfo.aidl b/core/java/android/hardware/location/HubInfo.aidl
new file mode 100644
index 0000000..25b5b0a
--- /dev/null
+++ b/core/java/android/hardware/location/HubInfo.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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.hardware.location;
+
+/** @hide */
+parcelable HubInfo;
diff --git a/core/java/android/hardware/location/HubInfo.java b/core/java/android/hardware/location/HubInfo.java
new file mode 100644
index 0000000..f7de127
--- /dev/null
+++ b/core/java/android/hardware/location/HubInfo.java
@@ -0,0 +1,153 @@
+/*
+ * 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.hardware.location;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.chre.flags.Flags;
+import android.os.BadParcelableException;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Union type for {@link ContextHubInfo} and {@link VendorHubInfo}
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public final class HubInfo implements Parcelable {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {TYPE_CONTEXT_HUB, TYPE_VENDOR_HUB})
+    private @interface HubType {}
+
+    public static final int TYPE_CONTEXT_HUB = 0;
+    public static final int TYPE_VENDOR_HUB = 1;
+
+    private final long mId;
+    @HubType private final int mType;
+    @Nullable private final ContextHubInfo mContextHubInfo;
+    @Nullable private final VendorHubInfo mVendorHubInfo;
+
+    /** @hide */
+    public HubInfo(long id, @NonNull ContextHubInfo contextHubInfo) {
+        mId = id;
+        mType = TYPE_CONTEXT_HUB;
+        mContextHubInfo = contextHubInfo;
+        mVendorHubInfo = null;
+    }
+
+    /** @hide */
+    public HubInfo(long id, @NonNull VendorHubInfo vendorHubInfo) {
+        mId = id;
+        mType = TYPE_VENDOR_HUB;
+        mContextHubInfo = null;
+        mVendorHubInfo = vendorHubInfo;
+    }
+
+    private HubInfo(Parcel in) {
+        mId = in.readLong();
+        mType = in.readInt();
+
+        switch (mType) {
+            case TYPE_CONTEXT_HUB:
+                mContextHubInfo = ContextHubInfo.CREATOR.createFromParcel(in);
+                mVendorHubInfo = null;
+                break;
+            case TYPE_VENDOR_HUB:
+                mVendorHubInfo = VendorHubInfo.CREATOR.createFromParcel(in);
+                mContextHubInfo = null;
+                break;
+            default:
+                throw new BadParcelableException("Parcelable has invalid type");
+        }
+    }
+
+    /** Get the hub unique identifier */
+    public long getId() {
+        return mId;
+    }
+
+    /** Get the hub type. The type can be {@link TYPE_CONTEXT_HUB} or {@link TYPE_VENDOR_HUB} */
+    public int getType() {
+        return mType;
+    }
+
+    /** Get the {@link ContextHubInfo} object, null if type is not {@link TYPE_CONTEXT_HUB} */
+    @Nullable
+    public ContextHubInfo getContextHubInfo() {
+        return mContextHubInfo;
+    }
+
+    /** Parcel implementation details */
+    public int describeContents() {
+        if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) {
+            return mContextHubInfo.describeContents();
+        }
+        if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) {
+            return mVendorHubInfo.describeContents();
+        }
+        return 0;
+    }
+
+    /** Parcel implementation details */
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeLong(mId);
+        out.writeInt(mType);
+
+        if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) {
+            mContextHubInfo.writeToParcel(out, flags);
+        }
+
+        if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) {
+            mVendorHubInfo.writeToParcel(out, flags);
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder();
+        out.append("HubInfo ID: 0x");
+        out.append(Long.toHexString(mId));
+        out.append("\n");
+        if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) {
+            out.append(" ContextHubDetails: ");
+            out.append(mContextHubInfo);
+        }
+        if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) {
+            out.append(" VendorHubDetails: ");
+            out.append(mVendorHubInfo);
+        }
+        return out.toString();
+    }
+
+    public static final @NonNull Creator<HubInfo> CREATOR =
+            new Creator<>() {
+                public HubInfo createFromParcel(Parcel in) {
+                    return new HubInfo(in);
+                }
+
+                public HubInfo[] newArray(int size) {
+                    return new HubInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index 11f30461..b0cc763 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -18,6 +18,7 @@
 
 // Declare any non-default types here with import statements
 import android.app.PendingIntent;
+import android.hardware.location.HubInfo;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
 import android.hardware.location.NanoApp;
@@ -82,6 +83,10 @@
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     List<ContextHubInfo> getContextHubs();
 
+    // Returns a list of HubInfo objects of available hubs (including ContextHub and VendorHub)
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    List<HubInfo> getHubs();
+
     // Loads a nanoapp at the specified hub (new API)
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     void loadNanoAppOnHub(
diff --git a/core/java/android/hardware/location/VendorHubInfo.aidl b/core/java/android/hardware/location/VendorHubInfo.aidl
new file mode 100644
index 0000000..a7936ac
--- /dev/null
+++ b/core/java/android/hardware/location/VendorHubInfo.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright 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.hardware.location;
+
+/** @hide */
+parcelable VendorHubInfo;
\ No newline at end of file
diff --git a/core/java/android/hardware/location/VendorHubInfo.java b/core/java/android/hardware/location/VendorHubInfo.java
new file mode 100644
index 0000000..26772b1
--- /dev/null
+++ b/core/java/android/hardware/location/VendorHubInfo.java
@@ -0,0 +1,95 @@
+/*
+ * 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.hardware.location;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.chre.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableHolder;
+
+/**
+ * Information about a VendorHub. VendorHub is similar to ContextHub, but it does not run the
+ * Context Hub Runtime Environment (or nano apps). It provides a unified endpoint messaging API
+ * through the ContextHub V4 HAL.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public final class VendorHubInfo implements Parcelable {
+    private final String mName;
+    private final int mVersion;
+    private final ParcelableHolder mExtendedInfo;
+
+    /** @hide */
+    public VendorHubInfo(android.hardware.contexthub.VendorHubInfo halHubInfo) {
+        mName = halHubInfo.name;
+        mVersion = halHubInfo.version;
+        mExtendedInfo = halHubInfo.extendedInfo;
+    }
+
+    private VendorHubInfo(Parcel in) {
+        mName = in.readString();
+        mVersion = in.readInt();
+        mExtendedInfo = ParcelableHolder.CREATOR.createFromParcel(in);
+    }
+
+    /** Get the hub name */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /** Get the hub version */
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /** Parcel implementation details */
+    public int describeContents() {
+        return mExtendedInfo.describeContents();
+    }
+
+    /** Parcel implementation details */
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(mName);
+        out.writeInt(mVersion);
+        mExtendedInfo.writeToParcel(out, flags);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder();
+        out.append("VendorHub Name : ");
+        out.append(mName);
+        out.append(", Version : ");
+        out.append(mVersion);
+        return out.toString();
+    }
+
+    public static final @NonNull Creator<VendorHubInfo> CREATOR =
+            new Creator<>() {
+                public VendorHubInfo createFromParcel(Parcel in) {
+                    return new VendorHubInfo(in);
+                }
+
+                public VendorHubInfo[] newArray(int size) {
+                    return new VendorHubInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index acda0c5..69bd668 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -55,7 +55,7 @@
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("MessageQueue_host")
+@RavenwoodRedirectionClass("MessageQueue_ravenwood")
 public final class MessageQueue {
     private static final String TAG_L = "LegacyMessageQueue";
     private static final String TAG_C = "ConcurrentMessageQueue";
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index 9db88d1..c2a47d7 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -54,7 +54,7 @@
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("MessageQueue_host")
+@RavenwoodRedirectionClass("MessageQueue_ravenwood")
 public final class MessageQueue {
     private static final String TAG = "ConcurrentMessageQueue";
     private static final boolean DEBUG = false;
diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl
index 6aa9852..ecb5e6f 100644
--- a/core/java/android/os/IVibratorManagerService.aidl
+++ b/core/java/android/os/IVibratorManagerService.aidl
@@ -17,13 +17,17 @@
 package android.os;
 
 import android.os.CombinedVibration;
+import android.os.ICancellationSignal;
 import android.os.IVibratorStateListener;
 import android.os.VibrationAttributes;
 import android.os.VibratorInfo;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
 
 /** {@hide} */
 interface IVibratorManagerService {
     int[] getVibratorIds();
+    int getCapabilities();
     VibratorInfo getVibratorInfo(int vibratorId);
     @EnforcePermission("ACCESS_VIBRATOR_STATE")
     boolean isVibrating(int vibratorId);
@@ -50,4 +54,9 @@
     oneway void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
             int constant, int inputDeviceId, int inputSource, String reason, int flags,
             int privFlags);
+
+    @EnforcePermission(allOf={"VIBRATE", "VIBRATE_VENDOR_EFFECTS", "START_VIBRATION_SESSIONS"})
+    ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg,
+            in int[] vibratorIds, in VibrationAttributes attributes, String reason,
+            in IVibrationSessionCallback callback);
 }
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 9f7b0b7..cae82d0 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -45,7 +45,7 @@
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("MessageQueue_host")
+@RavenwoodRedirectionClass("MessageQueue_ravenwood")
 public final class MessageQueue {
     private static final String TAG = "MessageQueue";
     private static final boolean DEBUG = false;
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
index f3eec13..2401f3d 100644
--- a/core/java/android/os/LockedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -48,7 +48,7 @@
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("MessageQueue_host")
+@RavenwoodRedirectionClass("MessageQueue_ravenwood")
 public final class MessageQueue {
     private static final String TAG = "LockedMessageQueue";
     private static final boolean DEBUG = false;
diff --git a/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java b/core/java/android/os/MessageQueue_ravenwood.java
similarity index 87%
rename from ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
rename to core/java/android/os/MessageQueue_ravenwood.java
index 1b63adc..4033707 100644
--- a/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
+++ b/core/java/android/os/MessageQueue_ravenwood.java
@@ -16,13 +16,16 @@
 
 package android.os;
 
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
 
-public class MessageQueue_host {
+@RavenwoodKeepWholeClass
+class MessageQueue_ravenwood {
     private static final AtomicLong sNextId = new AtomicLong(1);
-    private static final Map<Long, MessageQueue_host> sInstances = new ConcurrentHashMap<>();
+    private static final Map<Long, MessageQueue_ravenwood> sInstances = new ConcurrentHashMap<>();
 
     private boolean mDeleted = false;
 
@@ -37,8 +40,8 @@
         }
     }
 
-    private static MessageQueue_host getInstance(long id) {
-        MessageQueue_host q = sInstances.get(id);
+    private static MessageQueue_ravenwood getInstance(long id) {
+        MessageQueue_ravenwood q = sInstances.get(id);
         if (q == null) {
             throw new RuntimeException("MessageQueue doesn't exist with id=" + id);
         }
@@ -48,7 +51,7 @@
 
     public static long nativeInit() {
         final long id = sNextId.getAndIncrement();
-        final MessageQueue_host q = new MessageQueue_host();
+        final MessageQueue_ravenwood q = new MessageQueue_ravenwood();
         sInstances.put(id, q);
         return id;
     }
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index db323dc..435c34f 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -52,7 +52,7 @@
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("MessageQueue_host")
+@RavenwoodRedirectionClass("MessageQueue_ravenwood")
 public final class MessageQueue {
     private static final String TAG = "SemiConcurrentMessageQueue";
     private static final boolean DEBUG = false;
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index 011a3ee..c3cddf3 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -18,8 +18,11 @@
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
+import android.hardware.vibrator.IVibratorManager;
+import android.os.vibrator.VendorVibrationSession;
 import android.os.vibrator.VibratorInfoFactory;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -53,6 +56,7 @@
     private final Object mLock = new Object();
     @GuardedBy("mLock")
     private VibratorInfo mVibratorInfo;
+    private int[] mVibratorIds;
 
     @UnsupportedAppUsage
     public SystemVibrator(Context context) {
@@ -71,7 +75,11 @@
                 Log.w(TAG, "Failed to retrieve vibrator info; no vibrator manager.");
                 return VibratorInfo.EMPTY_VIBRATOR_INFO;
             }
-            int[] vibratorIds = mVibratorManager.getVibratorIds();
+            int[] vibratorIds = getVibratorIds();
+            if (vibratorIds == null) {
+                Log.w(TAG, "Failed to retrieve vibrator info; error retrieving vibrator ids.");
+                return VibratorInfo.EMPTY_VIBRATOR_INFO;
+            }
             if (vibratorIds.length == 0) {
                 // It is known that the device has no vibrator, so cache and return info that
                 // reflects the lack of support for effects/primitives.
@@ -95,20 +103,22 @@
 
     @Override
     public boolean hasVibrator() {
-        if (mVibratorManager == null) {
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
             Log.w(TAG, "Failed to check if vibrator exists; no vibrator manager.");
             return false;
         }
-        return mVibratorManager.getVibratorIds().length > 0;
+        return vibratorIds.length > 0;
     }
 
     @Override
     public boolean isVibrating() {
-        if (mVibratorManager == null) {
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
             Log.w(TAG, "Failed to vibrate; no vibrator manager.");
             return false;
         }
-        for (int vibratorId : mVibratorManager.getVibratorIds()) {
+        for (int vibratorId : vibratorIds) {
             if (mVibratorManager.getVibrator(vibratorId).isVibrating()) {
                 return true;
             }
@@ -136,6 +146,11 @@
             Log.w(TAG, "Failed to add vibrate state listener; no vibrator manager.");
             return;
         }
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
+            Log.w(TAG, "Failed to add vibrate state listener; error retrieving vibrator ids.");
+            return;
+        }
         MultiVibratorStateListener delegate = null;
         try {
             synchronized (mRegisteredListeners) {
@@ -145,7 +160,7 @@
                     return;
                 }
                 delegate = new MultiVibratorStateListener(executor, listener);
-                delegate.register(mVibratorManager);
+                delegate.register(mVibratorManager, vibratorIds);
                 mRegisteredListeners.put(listener, delegate);
                 delegate = null;
             }
@@ -184,6 +199,11 @@
     }
 
     @Override
+    public boolean areVendorSessionsSupported() {
+        return mVibratorManager.hasCapabilities(IVibratorManager.CAP_START_SESSIONS);
+    }
+
+    @Override
     public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, VibrationEffect effect,
             VibrationAttributes attrs) {
         if (mVibratorManager == null) {
@@ -243,6 +263,41 @@
         mVibratorManager.cancel(usageFilter);
     }
 
+    @Override
+    public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+            @NonNull VendorVibrationSession.Callback callback) {
+        if (mVibratorManager == null) {
+            Log.w(TAG, "Failed to start vibration session; no vibrator manager.");
+            executor.execute(
+                    () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR));
+            return;
+        }
+        int[] vibratorIds = getVibratorIds();
+        if (vibratorIds == null) {
+            Log.w(TAG, "Failed to start vibration session; error retrieving vibrator ids.");
+            executor.execute(
+                    () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR));
+            return;
+        }
+        mVibratorManager.startVendorSession(vibratorIds, attrs, reason, cancellationSignal,
+                executor, callback);
+    }
+
+    @Nullable
+    private int[] getVibratorIds() {
+        synchronized (mLock) {
+            if (mVibratorIds != null) {
+                return mVibratorIds;
+            }
+            if (mVibratorManager == null) {
+                Log.w(TAG, "Failed to retrieve vibrator ids; no vibrator manager.");
+                return null;
+            }
+            return mVibratorIds = mVibratorManager.getVibratorIds();
+        }
+    }
+
     /**
      * Tries to unregister individual {@link android.os.Vibrator.OnVibratorStateChangedListener}
      * that were left registered to vibrators after failures to register them to all vibrators.
@@ -319,8 +374,7 @@
         }
 
         /** Registers a listener to all individual vibrators in {@link VibratorManager}. */
-        public void register(VibratorManager vibratorManager) {
-            int[] vibratorIds = vibratorManager.getVibratorIds();
+        public void register(VibratorManager vibratorManager, @NonNull int[] vibratorIds) {
             synchronized (mLock) {
                 for (int i = 0; i < vibratorIds.length; i++) {
                     int vibratorId = vibratorIds[i];
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index a5697fb..f9935d2 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -22,6 +22,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.vibrator.IVibratorManager;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
+import android.os.vibrator.VendorVibrationSession;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
@@ -47,6 +51,8 @@
     @GuardedBy("mLock")
     private int[] mVibratorIds;
     @GuardedBy("mLock")
+    private int mCapabilities;
+    @GuardedBy("mLock")
     private final SparseArray<Vibrator> mVibrators = new SparseArray<>();
 
     @GuardedBy("mLock")
@@ -84,6 +90,11 @@
         }
     }
 
+    @Override
+    public boolean hasCapabilities(int capabilities) {
+        return (getCapabilities() & capabilities) == capabilities;
+    }
+
     @NonNull
     @Override
     public Vibrator getVibrator(int vibratorId) {
@@ -173,7 +184,7 @@
             int inputSource, String reason, int flags, int privFlags) {
         if (mService == null) {
             Log.w(TAG, "Failed to perform haptic feedback for input device;"
-                            + " no vibrator manager service.");
+                    + " no vibrator manager service.");
             return;
         }
         Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
@@ -197,6 +208,50 @@
         cancelVibration(usageFilter);
     }
 
+    @Override
+    public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs,
+            @Nullable String reason, @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) {
+        Objects.requireNonNull(vibratorIds);
+        VendorVibrationSessionCallbackDelegate callbackDelegate =
+                new VendorVibrationSessionCallbackDelegate(executor, callback);
+        if (mService == null) {
+            Log.w(TAG, "Failed to start vibration session; no vibrator manager service.");
+            callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR);
+            return;
+        }
+        try {
+            ICancellationSignal remoteCancellationSignal = mService.startVendorVibrationSession(
+                    mUid, mContext.getDeviceId(), mPackageName, vibratorIds, attrs, reason,
+                    callbackDelegate);
+            if (cancellationSignal != null) {
+                cancellationSignal.setRemote(remoteCancellationSignal);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to start vibration session.", e);
+            callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR);
+        }
+    }
+
+    private int getCapabilities() {
+        synchronized (mLock) {
+            if (mCapabilities != 0) {
+                return mCapabilities;
+            }
+            try {
+                if (mService == null) {
+                    Log.w(TAG, "Failed to retrieve vibrator manager capabilities;"
+                            + " no vibrator manager service.");
+                } else {
+                    return mCapabilities = mService.getCapabilities();
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+            return 0;
+        }
+    }
+
     private void cancelVibration(int usageFilter) {
         if (mService == null) {
             Log.w(TAG, "Failed to cancel vibration; no vibrator manager service.");
@@ -228,12 +283,45 @@
         }
     }
 
+    /** Callback for vendor vibration sessions. */
+    private static class VendorVibrationSessionCallbackDelegate extends
+            IVibrationSessionCallback.Stub {
+        private final Executor mExecutor;
+        private final VendorVibrationSession.Callback mCallback;
+
+        VendorVibrationSessionCallbackDelegate(
+                @NonNull Executor executor,
+                @NonNull VendorVibrationSession.Callback callback) {
+            Objects.requireNonNull(executor);
+            Objects.requireNonNull(callback);
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onStarted(IVibrationSession session) {
+            mExecutor.execute(() -> mCallback.onStarted(new VendorVibrationSession(session)));
+        }
+
+        @Override
+        public void onFinishing() {
+            mExecutor.execute(() -> mCallback.onFinishing());
+        }
+
+        @Override
+        public void onFinished(int status) {
+            mExecutor.execute(() -> mCallback.onFinished(status));
+        }
+    }
+
     /** Controls vibrations on a single vibrator. */
     private final class SingleVibrator extends Vibrator {
         private final VibratorInfo mVibratorInfo;
+        private final int[] mVibratorId;
 
         SingleVibrator(@NonNull VibratorInfo vibratorInfo) {
             mVibratorInfo = vibratorInfo;
+            mVibratorId = new int[]{mVibratorInfo.getId()};
         }
 
         @Override
@@ -252,6 +340,11 @@
         }
 
         @Override
+        public boolean areVendorSessionsSupported() {
+            return SystemVibratorManager.this.hasCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        }
+
+        @Override
         public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
                 @Nullable VibrationEffect effect, @Nullable VibrationAttributes attrs) {
             CombinedVibration combined = CombinedVibration.startParallel()
@@ -369,5 +462,13 @@
                 }
             }
         }
+
+        @Override
+        public void startVendorSession(@NonNull VibrationAttributes attrs, String reason,
+                @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+                @NonNull VendorVibrationSession.Callback callback) {
+            SystemVibratorManager.this.startVendorSession(mVibratorId, attrs, reason,
+                    cancellationSignal, executor, callback);
+        }
     }
 }
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index c4c4580..53f8a92 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -33,6 +33,7 @@
 import android.hardware.vibrator.IVibrator;
 import android.media.AudioAttributes;
 import android.os.vibrator.Flags;
+import android.os.vibrator.VendorVibrationSession;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibratorFrequencyProfile;
 import android.os.vibrator.VibratorFrequencyProfileLegacy;
@@ -247,6 +248,34 @@
     }
 
     /**
+     * Check whether the vibrator has support for vendor-specific effects.
+     *
+     * <p>Vendor vibration effects can be created via {@link VibrationEffect#createVendorEffect}.
+     *
+     * @return True if the hardware can play vendor-specific vibration effects, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public boolean areVendorEffectsSupported() {
+        return getInfo().hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
+    }
+
+    /**
+     * Check whether the vibrator has support for vendor-specific vibration sessions.
+     *
+     * <p>Vendor vibration sessions can be started via {@link #startVendorSession}.
+     *
+     * @return True if the hardware can play vendor-specific vibration sessions, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public boolean areVendorSessionsSupported() {
+        return false;
+    }
+
+    /**
      * Check whether the vibrator can be controlled by an external service with the
      * {@link IExternalVibratorService}.
      *
@@ -922,4 +951,44 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE)
     public void removeVibratorStateListener(@NonNull OnVibratorStateChangedListener listener) {
     }
+
+    /**
+     * Starts a vibration session in this vibrator.
+     *
+     * <p>The session will start asynchronously once the vibrator control can be acquired. Once it's
+     * started the {@link VendorVibrationSession} will be provided to the callback. This session
+     * should be used to play vibrations until the session is ended or canceled.
+     *
+     * <p>The vendor app will have exclusive control over the vibrator during this session. This
+     * control can be revoked by the vibrator service, which will be notified to the same session
+     * callback with the {@link VendorVibrationSession#STATUS_CANCELED}.
+     *
+     * <p>The {@link VibrationAttributes} will be used to decide the priority of the vendor
+     * vibrations that will be performed in this session. All vibrations within this session will
+     * apply the same attributes.
+     *
+     * @param attrs    The {@link VibrationAttributes} corresponding to the vibrations that will be
+     *                 performed in the session. This will be used to decide the priority of this
+     *                 session against other system vibrations.
+     * @param reason   The description for this session, used for debugging purposes.
+     * @param cancellationSignal A signal to cancel the session before it starts.
+     * @param executor The executor for the session callbacks.
+     * @param callback The {@link VendorVibrationSession.Callback} for the started session.
+     *
+     * @see VendorVibrationSession
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor,
+            @NonNull VendorVibrationSession.Callback callback) {
+        Log.w(TAG, "startVendorSession is not supported");
+        executor.execute(() -> callback.onFinished(VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
 }
diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java
index 0428876..0072bc2 100644
--- a/core/java/android/os/VibratorManager.java
+++ b/core/java/android/os/VibratorManager.java
@@ -22,9 +22,12 @@
 import android.annotation.SystemService;
 import android.app.ActivityThread;
 import android.content.Context;
+import android.os.vibrator.VendorVibrationSession;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 
+import java.util.concurrent.Executor;
+
 /**
  * Provides access to all vibrators from the device, as well as the ability to run them
  * in a synchronized fashion.
@@ -62,6 +65,14 @@
     public abstract int[] getVibratorIds();
 
     /**
+     * Return true if the vibrator manager has all capabilities, false otherwise.
+     * @hide
+     */
+    public boolean hasCapabilities(int capabilities) {
+        return false;
+    }
+
+    /**
      * Retrieve a single vibrator by id.
      *
      * @param vibratorId The id of the vibrator to be retrieved.
@@ -190,4 +201,30 @@
      */
     @RequiresPermission(android.Manifest.permission.VIBRATE)
     public abstract void cancel(int usageFilter);
+
+
+    /**
+     * Starts a vibration session on given vibrators.
+     *
+     * @param vibratorIds The vibrators that will be controlled by this session.
+     * @param attrs       The {@link VibrationAttributes} corresponding to the vibrations that will
+     *                    be performed in the session. This will be used to decide the priority of
+     *                    this session against other system vibrations.
+     * @param reason      The description for this session, used for debugging purposes.
+     * @param cancellationSignal A signal to cancel the session before it starts.
+     * @param executor    The executor for the session callbacks.
+     * @param callback    The {@link VendorVibrationSession.Callback} for the started session.
+     * @see Vibrator#startVendorSession
+     * @hide
+     */
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs,
+            @Nullable String reason, @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) {
+        Log.w(TAG, "startVendorSession is not supported");
+    }
 }
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 9c83bc2..d9db28e 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -243,6 +243,15 @@
 }
 
 flag {
+    name: "update_engine_api"
+    namespace: "art_mainline"
+    description: "Update Engine APIs for ART"
+    is_exported: true
+    is_fixed_read_only: true
+    bug: "377557749"
+}
+
+flag {
      namespace: "system_performance"
      name: "perfetto_sdk_tracing"
      description: "Tracing using Perfetto SDK."
diff --git a/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl b/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl
new file mode 100644
index 0000000..dbe5489
--- /dev/null
+++ b/core/java/android/os/instrumentation/ExecutableMethodFileOffsets.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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.os.instrumentation;
+
+/**
+ * Represents the location of the code for a compiled method within a process'
+ * memory.
+ * {@hide}
+ */
+@JavaDerive(toString=true)
+parcelable ExecutableMethodFileOffsets {
+  /**
+   * The OS path of the containing file (could be virtual).
+   */
+  @utf8InCpp String containerPath;
+  /**
+   * The offset of the containing file within the process' memory.
+   */
+  long containerOffset;
+  /**
+   * The offset of the method within the containing file.
+   */
+  long methodOffset;
+}
diff --git a/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
new file mode 100644
index 0000000..c45c51d
--- /dev/null
+++ b/core/java/android/os/instrumentation/IDynamicInstrumentationManager.aidl
@@ -0,0 +1,33 @@
+/*
+ * 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.os.instrumentation;
+
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.TargetProcess;
+
+/**
+ * System private API for managing the dynamic attachment of instrumentation.
+ *
+ * {@hide}
+ */
+interface IDynamicInstrumentationManager {
+    /** Provides ART metadata about the described compiled method within the target process */
+    @PermissionManuallyEnforced
+    @nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
+            in TargetProcess targetProcess, in MethodDescriptor methodDescriptor);
+}
diff --git a/core/java/android/os/instrumentation/MethodDescriptor.aidl b/core/java/android/os/instrumentation/MethodDescriptor.aidl
new file mode 100644
index 0000000..055d0ec
--- /dev/null
+++ b/core/java/android/os/instrumentation/MethodDescriptor.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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.os.instrumentation;
+
+/**
+ * Represents a JVM method, where class fields that make up its signature.
+ * {@hide}
+ */
+@JavaDerive(toString=true)
+parcelable MethodDescriptor {
+  /**
+    * Fully qualified class in reverse.domain.Naming
+    */
+  @utf8InCpp String fullyQualifiedClassName;
+  /**
+    * Name of the method.
+    */
+  @utf8InCpp String methodName;
+  /**
+    * Fully qualified types of method parameters, or string representations if primitive e.g. "int".
+    */
+  @utf8InCpp String[] fullyQualifiedParameters;
+}
diff --git a/core/java/android/os/instrumentation/TargetProcess.aidl b/core/java/android/os/instrumentation/TargetProcess.aidl
new file mode 100644
index 0000000..e90780d
--- /dev/null
+++ b/core/java/android/os/instrumentation/TargetProcess.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.instrumentation;
+
+/**
+ * Addresses a process that would run on the device.
+ * Helps disambiguate targeted processes in cases of pid re-use.
+ * {@hide}
+ */
+@JavaDerive(toString=true)
+parcelable TargetProcess {
+  int uid;
+  int pid;
+  @utf8InCpp String processName;
+}
diff --git a/core/java/android/os/vibrator/IVibrationSession.aidl b/core/java/android/os/vibrator/IVibrationSession.aidl
new file mode 100644
index 0000000..e829549
--- /dev/null
+++ b/core/java/android/os/vibrator/IVibrationSession.aidl
@@ -0,0 +1,55 @@
+/**
+ * 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.os.vibrator;
+
+import android.os.CombinedVibration;
+
+/**
+ * The communication channel by which an app control the system vibrators.
+ *
+ * In order to synchronize the places where vibrations might be controlled we provide this interface
+ * so the vibrator subsystem has a chance to:
+ *
+ * 1) Decide whether the current session should have the vibrator control.
+ * 2) Stop any on-going session for a new session/vibration, based on current system policy.
+ * {@hide}
+ */
+interface IVibrationSession {
+    const int STATUS_UNKNOWN = 0;
+    const int STATUS_SUCCESS = 1;
+    const int STATUS_IGNORED = 2;
+    const int STATUS_UNSUPPORTED = 3;
+    const int STATUS_CANCELED = 4;
+    const int STATUS_UNKNOWN_ERROR = 5;
+
+    /**
+     * A method called to start a vibration within this session. This will fail if the session
+     * is finishing or was canceled.
+     */
+    void vibrate(in CombinedVibration vibration, String reason);
+
+    /**
+     * A method called by the app to stop this session gracefully. The vibrator will complete any
+     * ongoing vibration before the session is ended.
+     */
+    void finishSession();
+
+    /**
+     * A method called by the app to stop this session immediatelly by interrupting any ongoing
+     * vibration.
+     */
+    void cancelSession();
+}
diff --git a/core/java/android/os/vibrator/IVibrationSessionCallback.aidl b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl
new file mode 100644
index 0000000..36c3695
--- /dev/null
+++ b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.os.vibrator;
+
+import android.os.vibrator.IVibrationSession;
+
+/**
+ * Callback for vibration session state.
+ * {@hide}
+ */
+oneway interface IVibrationSessionCallback {
+
+    /**
+     * A method called by the service after a vibration session has successfully started. After this
+     * is called the app has control over the vibrator through this given session.
+     */
+    void onStarted(in IVibrationSession session);
+
+    /**
+     * A method called by the service to indicate the session is ending and should no longer receive
+     * vibration requests.
+     */
+    void onFinishing();
+
+    /**
+     * A method called by the service after the session has ended. This might be triggered by the
+     * app or the service. The status code indicates the end reason.
+     */
+    void onFinished(int status);
+}
diff --git a/core/java/android/os/vibrator/VendorVibrationSession.java b/core/java/android/os/vibrator/VendorVibrationSession.java
new file mode 100644
index 0000000..c23f2ed
--- /dev/null
+++ b/core/java/android/os/vibrator/VendorVibrationSession.java
@@ -0,0 +1,236 @@
+/*
+ * 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.os.vibrator;
+
+import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.CombinedVibration;
+import android.os.RemoteException;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A vendor session that temporarily gains control over the system vibrators.
+ *
+ * <p>Vibration effects can be played by the vibrator in a vendor session via {@link #vibrate}. The
+ * effects will be forwarded to the vibrator hardware immediately. Any concurrency support is
+ * defined and controlled by the vibrator hardware implementation.
+ *
+ * <p>The session should be ended by {@link #close()}, which will wait until the last vibration ends
+ * and the vibrator is released. The end of the session will be notified to the {@link Callback}
+ * provided when the session was created.
+ *
+ * <p>Any ongoing session can be immediately interrupted by the vendor app via {@link #cancel()},
+ * including after {@link #close()} was called and the session is tearing down. A session can also
+ * be canceled by the vibrator service when it needs to regain control of the system vibrators.
+ *
+ * @see Vibrator#startVendorSession
+ * @hide
+ */
+@FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS)
+@SystemApi
+public final class VendorVibrationSession implements AutoCloseable {
+    private static final String TAG = "VendorVibrationSession";
+
+    /**
+     * The session ended successfully.
+     */
+    public static final int STATUS_SUCCESS = IVibrationSession.STATUS_SUCCESS;
+
+    /**
+     * The session was ignored.
+     *
+     * <p>This might be caused by user settings, vibration policies or the device state that
+     * prevents the app from performing vibrations for the requested
+     * {@link android.os.VibrationAttributes}.
+     */
+    public static final int STATUS_IGNORED = IVibrationSession.STATUS_IGNORED;
+
+    /**
+     * The session is not supported.
+     *
+     * <p>The support for vendor vibration sessions can be checked via
+     * {@link Vibrator#areVendorSessionsSupported()}.
+     */
+    public static final int STATUS_UNSUPPORTED = IVibrationSession.STATUS_UNSUPPORTED;
+
+    /**
+     * The session was canceled.
+     *
+     * <p>This might be triggered by the app after a session starts via {@link #cancel()}, or it
+     * can be triggered by the platform before or after the session has started.
+     */
+    public static final int STATUS_CANCELED = IVibrationSession.STATUS_CANCELED;
+
+    /**
+     * The session status is unknown.
+     */
+    public static final int STATUS_UNKNOWN = IVibrationSession.STATUS_UNKNOWN;
+
+    /**
+     * The session failed with unknown error.
+     *
+     * <p>This can be caused by a failure to start a vibration session or after it has started, to
+     * indicate it has ended unexpectedly because of a system failure.
+     */
+    public static final int STATUS_UNKNOWN_ERROR = IVibrationSession.STATUS_UNKNOWN_ERROR;
+
+    /** @hide */
+    @IntDef(prefix = { "STATUS_" }, value = {
+            STATUS_SUCCESS,
+            STATUS_IGNORED,
+            STATUS_UNSUPPORTED,
+            STATUS_CANCELED,
+            STATUS_UNKNOWN,
+            STATUS_UNKNOWN_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Status{}
+
+    private final IVibrationSession mSession;
+
+    /** @hide */
+    public VendorVibrationSession(@NonNull IVibrationSession session) {
+        Objects.requireNonNull(session);
+        mSession = session;
+    }
+
+    /**
+     * Vibrate with a given effect.
+     *
+     * <p>The vibration will be sent to the vibrator hardware immediately, without waiting for any
+     * previous vibration completion. The vendor should control the concurrency behavior at the
+     * hardware level (e.g. queueing, mixing, interrupting).
+     *
+     * <p>If the provided effect is played by the vibrator service with controlled timings (e.g.
+     * effects created via {@link VibrationEffect#createWaveform}), then triggering a new vibration
+     * will cause the ongoing playback to be interrupted in favor of the new vibration. If the
+     * effect is broken down into multiple consecutive commands (e.g. large primitive compositions)
+     * then the hardware commands will be triggered in succession without waiting for the completion
+     * callback.
+     *
+     * <p>The vendor app is responsible for timing the session requests and the vibrator hardware
+     * implementation is free to handle concurrency with different policies.
+     *
+     * @param effect The {@link VibrationEffect} describing the vibration to be performed.
+     * @param reason The description for the vibration reason, for debugging purposes.
+     */
+    @RequiresPermission(android.Manifest.permission.VIBRATE)
+    public void vibrate(@NonNull VibrationEffect effect, @Nullable String reason) {
+        try {
+            mSession.vibrate(CombinedVibration.createParallel(effect), reason);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to vibrate in a vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Cancel ongoing session.
+     *
+     * <p>This will stop the vibration immediately and return the vibrator control to the
+     * platform. This can also be triggered after {@link #close()} to immediately release the
+     * vibrator.
+     *
+     * <p>This will trigger {@link VendorVibrationSession.Callback#onFinished} directly with
+     * {@link #STATUS_CANCELED}.
+     */
+    public void cancel() {
+        try {
+            mSession.cancelSession();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to cancel vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * End ongoing session gracefully.
+     *
+     * <p>This might continue the vibration while it's ramping down and wrapping up the session
+     * in the vibrator hardware. No more vibration commands can be sent through this session
+     * after this method is called.
+     *
+     * <p>This will trigger {@link VendorVibrationSession.Callback#onFinishing()}.
+     */
+    @Override
+    public void close() {
+        try {
+            mSession.finishSession();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to finish vendor vibration session.", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callbacks for {@link VendorVibrationSession} events.
+     *
+     * @see Vibrator#startVendorSession
+     * @see VendorVibrationSession
+     */
+    public interface Callback {
+
+        /**
+         * New session was successfully started.
+         *
+         * <p>The vendor app can interact with the vibrator using the
+         * {@link VendorVibrationSession} provided.
+         */
+        void onStarted(@NonNull VendorVibrationSession session);
+
+        /**
+         * The session is ending and finishing any pending vibrations.
+         *
+         * <p>This is only invoked after {@link #onStarted(VendorVibrationSession)}. It will be
+         * triggered by both {@link VendorVibrationSession#cancel()} and
+         * {@link VendorVibrationSession#close()}. This might also be triggered if the platform
+         * cancels the ongoing session.
+         *
+         * <p>Session vibrations might be still ongoing in the vibrator hardware but the app can
+         * no longer send commands through the session. A finishing session can still be immediately
+         * stopped via calls to {@link VendorVibrationSession.Callback#cancel()}.
+         */
+        void onFinishing();
+
+        /**
+         * The session is finished.
+         *
+         * <p>The vibrator has finished any vibration and returned to the platform's control. This
+         * might be triggered by the vendor app or by the vibrator service.
+         *
+         * <p>If this is triggered before {@link #onStarted} then the session was finished before
+         * starting, either because it was cancelled or failed to start. If the session has already
+         * started then this will be triggered after {@link #onFinishing()} to indicate all session
+         * vibrations are complete and the vibrator is no longer under the session's control.
+         *
+         * @param status The session status.
+         */
+        void onFinished(@VendorVibrationSession.Status int status);
+    }
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 9e0d0e1..92c5c20 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -354,9 +354,10 @@
 flag {
     name: "health_connect_backup_restore_permission_enabled"
     is_fixed_read_only: true
-    namespace: "health_connect"
+    namespace: "health_fitness_aconfig"
     description: "This flag protects the permission that is required to call Health Connect backup and restore apis"
     bug: "376014879" # android_fr bug
+    is_exported: true
 }
 
 flag {
@@ -385,3 +386,17 @@
     description: "This fixed read-only flag is used to enable new ranging permission for all ranging use cases."
     bug: "370977414"
 }
+
+flag {
+    name: "system_selection_toolbar_enabled"
+    namespace: "permissions"
+    description: "Enables the system selection toolbar feature."
+    bug: "363318732"
+}
+
+flag {
+    name: "use_system_selection_toolbar_in_sysui"
+    namespace: "permissions"
+    description: "Uses the SysUi process to host the SelectionToolbarRenderService."
+    bug: "363318732"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d19681c..ef35171 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8687,6 +8687,19 @@
         public static final String ACCESSIBILITY_QS_TARGETS = "accessibility_qs_targets";
 
         /**
+         * Setting specifying the accessibility services, accessibility shortcut targets,
+         * or features to be toggled via a keyboard shortcut gesture.
+         *
+         * <p> This is a colon-separated string list which contains the flattened
+         * {@link ComponentName} and the class name of a system class implementing a supported
+         * accessibility feature.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_KEY_GESTURE_TARGETS =
+                "accessibility_key_gesture_targets";
+
+        /**
          * The system class name of magnification controller which is a target to be toggled via
          * accessibility shortcut or accessibility button.
          *
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 1d35344..7cb0ffc 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -120,3 +120,10 @@
     description: "Feature flag for exposing KeyStore grant APIs"
     bug: "351158708"
 }
+
+flag {
+    name: "secure_lockdown"
+    namespace: "biometrics"
+    description: "Feature flag for Secure Lockdown feature"
+    bug: "373422357"
+}
\ No newline at end of file
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 72f2de8..dfc11dc 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -67,3 +67,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "cleanup_dream_settings_on_uninstall"
+    namespace: "systemui"
+    description: "Cleans up dream settings if dream package is uninstalled."
+    bug: "338210427"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/service/settings/preferences/GetValueRequest.aidl b/core/java/android/service/settings/preferences/GetValueRequest.aidl
new file mode 100644
index 0000000..2a0eb09
--- /dev/null
+++ b/core/java/android/service/settings/preferences/GetValueRequest.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable GetValueRequest;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/GetValueRequest.java b/core/java/android/service/settings/preferences/GetValueRequest.java
new file mode 100644
index 0000000..4f82800
--- /dev/null
+++ b/core/java/android/service/settings/preferences/GetValueRequest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.flags.Flags;
+
+import java.util.Objects;
+
+/**
+ * Request parameters to retrieve the current value of a Settings Preference.
+ *
+ * <p>This object passed to {@link SettingsPreferenceService#onGetPreferenceValue} will result
+ * in a {@link GetValueResult}.
+ *
+ * <ul>
+ *   <li>{@link #getScreenKey} is a parameter to distinguish the container screen
+ *   of a preference as a preference key may not be unique within its application.
+ *   <li>{@link #getPreferenceKey} is a parameter to identify the preference for which the value is
+ *   being requested. These keys will be unique with their Preference Screen, but may not be unique
+ *   within their application, so it is required to pair this with {@link #getScreenKey} to
+ *   ensure this request matches the intended target.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class GetValueRequest implements Parcelable {
+
+    @NonNull
+    private final String mScreenKey;
+    @NonNull
+    private final String mPreferenceKey;
+
+    /**
+     * Returns the screen key of requested Preference.
+     */
+    @NonNull
+    public String getScreenKey() {
+        return mScreenKey;
+    }
+
+    /**
+     * Returns the key of requested Preference.
+     */
+    @NonNull
+    public String getPreferenceKey() {
+        return mPreferenceKey;
+    }
+
+    private GetValueRequest(@NonNull Builder builder) {
+        mScreenKey = builder.mScreenKey;
+        mPreferenceKey = builder.mPreferenceKey;
+    }
+
+    private GetValueRequest(@NonNull Parcel in) {
+        mScreenKey = Objects.requireNonNull(in.readString8());
+        mPreferenceKey = Objects.requireNonNull(in.readString8());
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mScreenKey);
+        dest.writeString8(mPreferenceKey);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link GetValueRequest}.
+     */
+    @NonNull
+    public static final Creator<GetValueRequest> CREATOR = new Creator<GetValueRequest>() {
+        @Override
+        public GetValueRequest createFromParcel(@NonNull Parcel in) {
+            return new GetValueRequest(in);
+        }
+
+        @Override
+        public GetValueRequest[] newArray(int size) {
+            return new GetValueRequest[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link GetValueRequest}.
+     */
+    public static final class Builder {
+        private final String mScreenKey;
+        private final String mPreferenceKey;
+
+        /**
+         * Create Builder instance.
+         * @param screenKey required to be not empty
+         * @param preferenceKey required to be not empty
+         */
+        public Builder(@NonNull String screenKey, @NonNull String preferenceKey) {
+            if (TextUtils.isEmpty(screenKey)) {
+                throw new IllegalArgumentException("screenKey cannot be empty");
+            }
+            if (TextUtils.isEmpty(preferenceKey)) {
+                throw new IllegalArgumentException("preferenceKey cannot be empty");
+            }
+            mScreenKey = screenKey;
+            mPreferenceKey = preferenceKey;
+        }
+
+        /**
+         * Constructs an immutable {@link GetValueRequest} object.
+         */
+        @NonNull
+        public GetValueRequest build() {
+            return new GetValueRequest(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/GetValueResult.aidl b/core/java/android/service/settings/preferences/GetValueResult.aidl
new file mode 100644
index 0000000..b5ebd35
--- /dev/null
+++ b/core/java/android/service/settings/preferences/GetValueResult.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable GetValueResult;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/GetValueResult.java b/core/java/android/service/settings/preferences/GetValueResult.java
new file mode 100644
index 0000000..369dea7
--- /dev/null
+++ b/core/java/android/service/settings/preferences/GetValueResult.java
@@ -0,0 +1,213 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Result object given a corresponding {@link GetValueRequest}.
+ * <ul>
+ *   <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK},
+ *   {@link #getValue} will be populated with the settings preference value and
+ *   {@link #getMetadata} will be populated with its metadata.
+ *   <li>If the request is unsuccessful, {@link #getResultCode} be a value other than
+ *   {@link #RESULT_OK} - see documentation for those possibilities to understand the cause
+ *   of the failure.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class GetValueResult implements Parcelable {
+
+    @ResultCode
+    private final int mResultCode;
+    @Nullable
+    private final SettingsPreferenceValue mValue;
+    @Nullable
+    private final SettingsPreferenceMetadata mMetadata;
+
+    /**
+     * Returns the result code indicating status of the request.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /**
+     * Returns the value of requested Preference if request successful.
+     */
+    @Nullable
+    public SettingsPreferenceValue getValue() {
+        return mValue;
+    }
+
+    /**
+     * Returns the metadata of requested Preference if request successful.
+     */
+    @Nullable
+    public SettingsPreferenceMetadata getMetadata() {
+        return mMetadata;
+    }
+
+    /** @hide */
+    @IntDef(prefix = { "RESULT_" }, value = {
+            RESULT_OK,
+            RESULT_UNSUPPORTED,
+            RESULT_UNAVAILABLE,
+            RESULT_REQUIRE_APP_PERMISSION,
+            RESULT_DISALLOW,
+            RESULT_INVALID_REQUEST,
+            RESULT_INTERNAL_ERROR,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {
+    }
+
+    /** Request is successful. */
+    public static final int RESULT_OK = 0;
+    /**
+     * Requested preference is not supported by this API.
+     * <p>Retry not advised.
+     */
+    public static final int RESULT_UNSUPPORTED = 1;
+    /**
+     * Preference is currently not available, likely due to device state or the state of
+     * a dependency.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_UNAVAILABLE = 2;
+    /**
+     * Requested preference requires permissions not held by the calling application.
+     * <p>Retry may succeed if necessary permissions are obtained.
+     */
+    public static final int RESULT_REQUIRE_APP_PERMISSION = 3;
+    /**
+     * Requested preference is not allowed for access in this API under the current device policy.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_DISALLOW = 4;
+    /**
+     * Request object is not valid.
+     * <p>Retry not advised with current parameters.
+     */
+    public static final int RESULT_INVALID_REQUEST = 5;
+    /**
+     * API call failed due to an issue with the service binding.
+     * <p>Retry may succeed.
+     */
+    public static final int RESULT_INTERNAL_ERROR = 6;
+
+
+    private GetValueResult(@NonNull Builder builder) {
+        mResultCode = builder.mResultCode;
+        mValue = builder.mValue;
+        mMetadata = builder.mMetadata;
+    }
+
+    private GetValueResult(@NonNull Parcel in) {
+        mResultCode = in.readInt();
+        mValue = in.readParcelable(SettingsPreferenceValue.class.getClassLoader(),
+                SettingsPreferenceValue.class);
+        mMetadata = in.readParcelable(SettingsPreferenceMetadata.class.getClassLoader(),
+                SettingsPreferenceMetadata.class);
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mResultCode);
+        dest.writeParcelable(mValue, flags);
+        dest.writeParcelable(mMetadata, flags);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link GetValueResult}.
+     */
+    @NonNull
+    public static final Creator<GetValueResult> CREATOR = new Creator<>() {
+        @Override
+        public GetValueResult createFromParcel(@NonNull Parcel in) {
+            return new GetValueResult(in);
+        }
+
+        @Override
+        public GetValueResult[] newArray(int size) {
+            return new GetValueResult[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link GetValueResult}.
+     */
+    public static final class Builder {
+        @ResultCode
+        private final int mResultCode;
+        private SettingsPreferenceValue mValue;
+        private SettingsPreferenceMetadata mMetadata;
+
+        /**
+         * Create Builder instance.
+         * @param resultCode indicates status of the request
+         */
+        public Builder(@ResultCode int resultCode) {
+            mResultCode = resultCode;
+        }
+
+        /**
+         * Sets the preference value on the result.
+         */
+        @NonNull
+        public Builder setValue(@Nullable SettingsPreferenceValue value) {
+            mValue = value;
+            return this;
+        }
+
+        /**
+         * Sets the metadata on the result.
+         */
+        @NonNull
+        public Builder setMetadata(@Nullable SettingsPreferenceMetadata metadata) {
+            mMetadata = metadata;
+            return this;
+        }
+
+        /**
+         * Constructs an immutable {@link GetValueResult} object.
+         */
+        @NonNull
+        public GetValueResult build() {
+            return new GetValueResult(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/IGetValueCallback.aidl b/core/java/android/service/settings/preferences/IGetValueCallback.aidl
new file mode 100644
index 0000000..bbc7423
--- /dev/null
+++ b/core/java/android/service/settings/preferences/IGetValueCallback.aidl
@@ -0,0 +1,9 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.GetValueResult;
+
+/** @hide */
+oneway interface IGetValueCallback {
+    void onSuccess(in GetValueResult result) = 1;
+    void onFailure() = 2;
+}
diff --git a/core/java/android/service/settings/preferences/IMetadataCallback.aidl b/core/java/android/service/settings/preferences/IMetadataCallback.aidl
new file mode 100644
index 0000000..3bd5ebe
--- /dev/null
+++ b/core/java/android/service/settings/preferences/IMetadataCallback.aidl
@@ -0,0 +1,9 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.MetadataResult;
+
+/** @hide */
+oneway interface IMetadataCallback {
+    void onSuccess(in MetadataResult result);
+    void onFailure();
+}
diff --git a/core/java/android/service/settings/preferences/ISetValueCallback.aidl b/core/java/android/service/settings/preferences/ISetValueCallback.aidl
new file mode 100644
index 0000000..0765660
--- /dev/null
+++ b/core/java/android/service/settings/preferences/ISetValueCallback.aidl
@@ -0,0 +1,9 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.SetValueResult;
+
+/** @hide */
+oneway interface ISetValueCallback {
+    void onSuccess(in SetValueResult result);
+    void onFailure();
+}
diff --git a/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
new file mode 100644
index 0000000..64a8b90
--- /dev/null
+++ b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
@@ -0,0 +1,18 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.GetValueRequest;
+import android.service.settings.preferences.IGetValueCallback;
+import android.service.settings.preferences.IMetadataCallback;
+import android.service.settings.preferences.ISetValueCallback;
+import android.service.settings.preferences.MetadataRequest;
+import android.service.settings.preferences.SetValueRequest;
+
+/** @hide */
+oneway interface ISettingsPreferenceService {
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getAllPreferenceMetadata(in MetadataRequest request, IMetadataCallback callback) = 1;
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getPreferenceValue(in GetValueRequest request, IGetValueCallback callback) = 2;
+    @EnforcePermission(allOf = {"READ_SYSTEM_PREFERENCES", "WRITE_SYSTEM_PREFERENCES"})
+    void setPreferenceValue(in SetValueRequest request, ISetValueCallback callback) = 3;
+}
diff --git a/core/java/android/service/settings/preferences/MetadataRequest.aidl b/core/java/android/service/settings/preferences/MetadataRequest.aidl
new file mode 100644
index 0000000..dc3cbc42
--- /dev/null
+++ b/core/java/android/service/settings/preferences/MetadataRequest.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable MetadataRequest;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/MetadataRequest.java b/core/java/android/service/settings/preferences/MetadataRequest.java
new file mode 100644
index 0000000..ffecc6b
--- /dev/null
+++ b/core/java/android/service/settings/preferences/MetadataRequest.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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.flags.Flags;
+
+/**
+ * Request parameters to retrieve all metadata for all available settings preferences within this
+ * application.
+ *
+ * <p>This object passed to {@link SettingsPreferenceService#onGetAllPreferenceMetadata} will result
+ * in a {@link MetadataResult}.
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class MetadataRequest implements Parcelable {
+    private MetadataRequest() {}
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link MetadataRequest}.
+     */
+    @NonNull
+    public static final Creator<MetadataRequest> CREATOR = new Creator<>() {
+        @Override
+        public MetadataRequest createFromParcel(@NonNull Parcel in) {
+            return new MetadataRequest();
+        }
+
+        @Override
+        public MetadataRequest[] newArray(int size) {
+            return new MetadataRequest[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link MetadataRequest}.
+     */
+    public static final class Builder {
+        /** Constructs an immutable {@link MetadataRequest} object. */
+        @NonNull
+        public MetadataRequest build() {
+            return new MetadataRequest();
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/MetadataResult.aidl b/core/java/android/service/settings/preferences/MetadataResult.aidl
new file mode 100644
index 0000000..af9e8a8
--- /dev/null
+++ b/core/java/android/service/settings/preferences/MetadataResult.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable MetadataResult;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/MetadataResult.java b/core/java/android/service/settings/preferences/MetadataResult.java
new file mode 100644
index 0000000..6a65dcc
--- /dev/null
+++ b/core/java/android/service/settings/preferences/MetadataResult.java
@@ -0,0 +1,164 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Result object given a corresponding {@link MetadataRequest}.
+ * <ul>
+ *   <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK} and
+ *   {@link #getMetadataList} will be populated with metadata for all available preferences within
+ *   this application.
+ *   <li>If the request is unsuccessful, {@link #getResultCode} be a value other than
+ *   {@link #RESULT_OK} - see documentation for those possibilities to understand the cause
+ *   of the failure.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class MetadataResult implements Parcelable {
+
+    @ResultCode
+    private final int mResultCode;
+    @NonNull
+    private final List<SettingsPreferenceMetadata> mMetadataList;
+
+    /**
+     * Returns the result code indicating status of the request.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /**
+     * Returns the list of available Preference Metadata.
+     * <p>This instance is shared so this list should not be modified.
+     */
+    @NonNull
+    public List<SettingsPreferenceMetadata> getMetadataList() {
+        return mMetadataList;
+    }
+
+    /** @hide */
+    @IntDef(prefix = { "RESULT_" }, value = {
+            RESULT_OK,
+            RESULT_UNSUPPORTED,
+            RESULT_INTERNAL_ERROR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {
+    }
+
+    /** Request is successful. */
+    public static final int RESULT_OK = 0;
+    /**
+     * No preferences in this application support this API.
+     * <p>Retry not advised.
+     */
+    public static final int RESULT_UNSUPPORTED = 1;
+    /**
+     * API call failed due to an issue with the service binding.
+     * <p>Retry may succeed.
+     */
+    public static final int RESULT_INTERNAL_ERROR = 2;
+
+    private MetadataResult(@NonNull Builder builder) {
+        mResultCode = builder.mResultCode;
+        mMetadataList = builder.mMetadataList;
+    }
+    private MetadataResult(@NonNull Parcel in) {
+        mResultCode = in.readInt();
+        mMetadataList = new ArrayList<>();
+        in.readTypedList(mMetadataList, SettingsPreferenceMetadata.CREATOR);
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mResultCode);
+        dest.writeTypedList(mMetadataList, flags);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link MetadataResult}.
+     */
+    @NonNull
+    public static final Creator<MetadataResult> CREATOR = new Creator<>() {
+        @Override
+        public MetadataResult createFromParcel(@NonNull Parcel in) {
+            return new MetadataResult(in);
+        }
+
+        @Override
+        public MetadataResult[] newArray(int size) {
+            return new MetadataResult[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link MetadataResult}.
+     */
+    public static final class Builder {
+        @ResultCode
+        private final int mResultCode;
+        private List<SettingsPreferenceMetadata> mMetadataList = Collections.emptyList();
+
+        /**
+         * Create Builder instance.
+         * @param resultCode indicates status of the request
+         */
+        public Builder(@ResultCode int resultCode) {
+            mResultCode = resultCode;
+        }
+
+        /**
+         * Sets the metadata list on the result.
+         */
+        @NonNull
+        public Builder setMetadataList(@NonNull List<SettingsPreferenceMetadata> metadataList) {
+            mMetadataList = metadataList;
+            return this;
+        }
+
+        /**
+         * Constructs an immutable {@link MetadataResult} object.
+         */
+        @NonNull
+        public MetadataResult build() {
+            return new MetadataResult(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/SetValueRequest.aidl b/core/java/android/service/settings/preferences/SetValueRequest.aidl
new file mode 100644
index 0000000..198e333
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SetValueRequest.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable SetValueRequest;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/SetValueRequest.java b/core/java/android/service/settings/preferences/SetValueRequest.java
new file mode 100644
index 0000000..f7600ae
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SetValueRequest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.flags.Flags;
+
+import java.util.Objects;
+
+/**
+ * Request parameters to set the current value to a Settings Preference.
+ * <p>This object passed to {@link SettingsPreferenceService#onSetPreferenceValue} will result in a
+ * {@link SetValueResult}.
+ * <ul>
+ *   <li>{@link #getScreenKey} is a parameter to distinguish the container screen
+ *   of a preference as a preference key may not be unique within its application.
+ *   <li>{@link #getPreferenceKey} is a parameter to identify the preference for which the value is
+ *   being requested. These keys will be unique with their Preference Screen, but may not be unique
+ *   within their application, so it is required to pair this with {@link #getScreenKey} to
+ *   ensure this request matches the intended target.
+ *   <li>{@link #getPreferenceValue} is a parameter to specify the value that this request aims to
+ *   set. If this value is invalid (malformed or does not match the type of the preference) then
+ *   this request will fail.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class SetValueRequest implements Parcelable {
+
+    @NonNull
+    private final String mScreenKey;
+    @NonNull
+    private final String mPreferenceKey;
+    @NonNull
+    private final SettingsPreferenceValue mPreferenceValue;
+
+    /**
+     * Returns the screen key of requested Preference.
+     */
+    @NonNull
+    public String getScreenKey() {
+        return mScreenKey;
+    }
+
+    /**
+     * Returns the key of requested Preference.
+     */
+    @NonNull
+    public String getPreferenceKey() {
+        return mPreferenceKey;
+    }
+
+    /**
+     * Returns the value of requested Preference.
+     */
+    @NonNull
+    public SettingsPreferenceValue getPreferenceValue() {
+        return mPreferenceValue;
+    }
+
+    private SetValueRequest(@NonNull Builder builder) {
+        mScreenKey = builder.mScreenKey;
+        mPreferenceKey = builder.mPreferenceKey;
+        mPreferenceValue = builder.mPreferenceValue;
+    }
+
+    private SetValueRequest(@NonNull Parcel in) {
+        mScreenKey = Objects.requireNonNull(in.readString8());
+        mPreferenceKey = Objects.requireNonNull(in.readString8());
+        mPreferenceValue = Objects.requireNonNull(in.readParcelable(
+                SettingsPreferenceValue.class.getClassLoader(), SettingsPreferenceValue.class));
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mScreenKey);
+        dest.writeString8(mPreferenceKey);
+        dest.writeParcelable(mPreferenceValue, flags);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link SetValueRequest}.
+     */
+    @NonNull
+    public static final Creator<SetValueRequest> CREATOR = new Creator<SetValueRequest>() {
+        @Override
+        public SetValueRequest createFromParcel(@NonNull Parcel in) {
+            return new SetValueRequest(in);
+        }
+
+        @Override
+        public SetValueRequest[] newArray(int size) {
+            return new SetValueRequest[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link SetValueRequest}.
+     */
+    public static final class Builder {
+        private final String mScreenKey;
+        private final String mPreferenceKey;
+        private final SettingsPreferenceValue mPreferenceValue;
+
+        /**
+         * Create Builder instance.
+         * @param screenKey required to be not empty
+         * @param preferenceKey required to be not empty
+         * @param value value to set to requested Preference
+         */
+        public Builder(@NonNull String screenKey, @NonNull String preferenceKey,
+                       @NonNull SettingsPreferenceValue value) {
+            if (TextUtils.isEmpty(screenKey)) {
+                throw new IllegalArgumentException("screenKey cannot be empty");
+            }
+            if (TextUtils.isEmpty(preferenceKey)) {
+                throw new IllegalArgumentException("preferenceKey cannot be empty");
+            }
+            mScreenKey = screenKey;
+            mPreferenceKey = preferenceKey;
+            mPreferenceValue = value;
+        }
+
+        /**
+         * Constructs an immutable {@link SetValueRequest} object.
+         */
+        @NonNull
+        public SetValueRequest build() {
+            return new SetValueRequest(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/SetValueResult.aidl b/core/java/android/service/settings/preferences/SetValueResult.aidl
new file mode 100644
index 0000000..f548134
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SetValueResult.aidl
@@ -0,0 +1,4 @@
+package android.service.settings.preferences;
+
+/** @hide */
+parcelable SetValueResult;
\ No newline at end of file
diff --git a/core/java/android/service/settings/preferences/SetValueResult.java b/core/java/android/service/settings/preferences/SetValueResult.java
new file mode 100644
index 0000000..cb1776a
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SetValueResult.java
@@ -0,0 +1,179 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Result object given a corresponding {@link SetValueRequest}.
+ * <ul>
+ *   <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK}.
+ *   <li>If the request is unsuccessful, {@link #getResultCode} be a value other than
+ *   {@link #RESULT_OK} - see documentation for those possibilities to understand the cause
+ *   of the failure.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class SetValueResult implements Parcelable {
+
+    @ResultCode
+    private final int mResultCode;
+
+    /**
+     * Returns the result code indicating status of the request.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+
+    /** @hide */
+    @IntDef(prefix = { "RESULT_" }, value = {
+            RESULT_OK,
+            RESULT_UNSUPPORTED,
+            RESULT_DISABLED,
+            RESULT_RESTRICTED,
+            RESULT_UNAVAILABLE,
+            RESULT_REQUIRE_APP_PERMISSION,
+            RESULT_REQUIRE_USER_CONSENT,
+            RESULT_DISALLOW,
+            RESULT_INVALID_REQUEST,
+            RESULT_INTERNAL_ERROR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResultCode {
+    }
+
+    /** Request is successful and the value was set. */
+    public static final int RESULT_OK = 0;
+    /**
+     * Requested preference is not supported by this API.
+     * <p>Retry not advised.
+     */
+    public static final int RESULT_UNSUPPORTED = 1;
+    /**
+     * Requested preference is disabled, thus unable to be set in this state.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_DISABLED = 2;
+    /**
+     * Requested preference is restricted, thus unable to be set under this policy.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_RESTRICTED = 3;
+    /**
+     * Preference is currently not available, likely due to device state or the state of
+     * a dependency.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_UNAVAILABLE = 4;
+    /**
+     * Requested preference requires permissions not held by the calling application.
+     * <p>Retry may succeed if necessary permissions are obtained.
+     */
+    public static final int RESULT_REQUIRE_APP_PERMISSION = 5;
+    /**
+     * User consent was not approved for this operation.
+     * <p>Retry may succeed if user provides consent.
+     */
+    public static final int RESULT_REQUIRE_USER_CONSENT = 6;
+    /**
+     * Requested preference is not allowed for access in this API under the current device policy.
+     * <p>Retry may succeed if underlying conditions change.
+     */
+    public static final int RESULT_DISALLOW = 7;
+    /**
+     * Request object is not valid.
+     * <p>Retry not advised with current parameters.
+     */
+    public static final int RESULT_INVALID_REQUEST = 8;
+    /**
+     * API call failed due to an issue with the service binding.
+     * <p>Retry may succeed.
+     */
+    public static final int RESULT_INTERNAL_ERROR = 9;
+
+    private SetValueResult(@NonNull Builder builder) {
+        mResultCode = builder.mResultCode;
+    }
+
+    private SetValueResult(@NonNull Parcel in) {
+        mResultCode = in.readInt();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mResultCode);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link SetValueResult}.
+     */
+    @NonNull
+    public static final Creator<SetValueResult> CREATOR = new Creator<>() {
+        @Override
+        public SetValueResult createFromParcel(@NonNull Parcel in) {
+            return new SetValueResult(in);
+        }
+
+        @Override
+        public SetValueResult[] newArray(int size) {
+            return new SetValueResult[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link SetValueResult}.
+     */
+    public static final class Builder {
+        @ResultCode
+        private final int mResultCode;
+
+        /**
+         * Create Builder instance.
+         * @param resultCode indicates status of the request
+         */
+        public Builder(@ResultCode int resultCode) {
+            mResultCode = resultCode;
+        }
+
+        /**
+         * Constructs an immutable {@link SetValueResult} object.
+         */
+        @NonNull
+        public SetValueResult build() {
+            return new SetValueResult(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java
new file mode 100644
index 0000000..1d08c52
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java
@@ -0,0 +1,436 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Data object representation of a Settings Preference definition and state.
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class SettingsPreferenceMetadata implements Parcelable {
+
+    @NonNull
+    private final String mKey;
+    @NonNull
+    private final String mScreenKey;
+    @Nullable
+    private final String mTitle;
+    @Nullable
+    private final String mSummary;
+    @NonNull
+    private final List<String> mBreadcrumbs;
+    @NonNull
+    private final List<String> mReadPermissions;
+    @NonNull
+    private final List<String> mWritePermissions;
+    private final boolean mEnabled;
+    private final boolean mAvailable;
+    private final boolean mWritable;
+    private final boolean mRestricted;
+    private final int mSensitivity;
+    @Nullable
+    private final PendingIntent mLaunchIntent;
+    @NonNull
+    private final Bundle mExtras;
+
+    /**
+     * Returns the key of Preference.
+     */
+    @NonNull
+    public String getKey() {
+        return mKey;
+    }
+
+    /**
+     * Returns the screen key of Preference.
+     */
+    @NonNull
+    public String getScreenKey() {
+        return mScreenKey;
+    }
+
+    /**
+     * Returns the title of Preference.
+     */
+    @Nullable
+    public String getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Returns the summary of Preference.
+     */
+    @Nullable
+    public String getSummary() {
+        return mSummary;
+    }
+
+    /**
+     * Returns the breadcrumbs (navigation context) of Preference.
+     * <p>May be empty.
+     */
+    @NonNull
+    public List<String> getBreadcrumbs() {
+        return mBreadcrumbs;
+    }
+
+    /**
+     * Returns the permissions required to read this Preference's value.
+     * <p>May be empty.
+     */
+    @NonNull
+    public List<String> getReadPermissions() {
+        return mReadPermissions;
+    }
+
+    /**
+     * Returns the permissions required to write this Preference's value.
+     * <p>May be empty.
+     */
+    @NonNull
+    public List<String> getWritePermissions() {
+        return mWritePermissions;
+    }
+
+    /**
+     * Returns whether Preference is enabled.
+     */
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    /**
+     * Returns whether Preference is available.
+     */
+    public boolean isAvailable() {
+        return mAvailable;
+    }
+
+    /**
+     * Returns whether Preference is writable.
+     */
+    public boolean isWritable() {
+        return mWritable;
+    }
+
+    /**
+     * Returns whether Preference is restricted.
+     */
+    public boolean isRestricted() {
+        return mRestricted;
+    }
+
+    /**
+     * Returns the write-level sensitivity of Preference.
+     */
+    @WriteSensitivity
+    public int getWriteSensitivity() {
+        return mSensitivity;
+    }
+
+    /**
+     * Returns the intent to launch the host app page for this Preference.
+     */
+    @Nullable
+    public PendingIntent getLaunchIntent() {
+        return mLaunchIntent;
+    }
+
+    /**
+     * Returns any additional fields specific to this preference.
+     * <p>Treat all data as optional.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /** @hide */
+    @IntDef(value = {
+            NOT_SENSITIVE,
+            SENSITIVE,
+            INTENT_ONLY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WriteSensitivity {}
+
+    /**
+     * Preference is not sensitive, thus its value is writable without explicit consent, assuming
+     * all necessary permissions are granted.
+     */
+    public static final int NOT_SENSITIVE = 0;
+    /**
+     * Preference is sensitive, meaning that in addition to necessary permissions, writing its value
+     * will also request explicit user consent.
+     */
+    public static final int SENSITIVE = 1;
+    /**
+     * Preference is not permitted for write-access via API and must be changed via Settings page.
+     */
+    public static final int INTENT_ONLY = 2;
+
+    private SettingsPreferenceMetadata(@NonNull Builder builder) {
+        mKey = builder.mKey;
+        mScreenKey = builder.mScreenKey;
+        mTitle = builder.mTitle;
+        mSummary = builder.mSummary;
+        mBreadcrumbs = builder.mBreadcrumbs;
+        mReadPermissions = builder.mReadPermissions;
+        mWritePermissions = builder.mWritePermissions;
+        mEnabled = builder.mEnabled;
+        mAvailable = builder.mAvailable;
+        mWritable = builder.mWritable;
+        mRestricted = builder.mRestricted;
+        mSensitivity = builder.mSensitivity;
+        mLaunchIntent = builder.mLaunchIntent;
+        mExtras = Objects.requireNonNullElseGet(builder.mExtras, Bundle::new);
+    }
+    @SuppressLint("ParcelClassLoader")
+    private SettingsPreferenceMetadata(@NonNull Parcel in) {
+        mKey = Objects.requireNonNull(in.readString8());
+        mScreenKey = Objects.requireNonNull(in.readString8());
+        mTitle = in.readString8();
+        mSummary = in.readString8();
+        mBreadcrumbs = new ArrayList<>();
+        in.readStringList(mBreadcrumbs);
+        mReadPermissions = new ArrayList<>();
+        in.readStringList(mReadPermissions);
+        mWritePermissions = new ArrayList<>();
+        in.readStringList(mWritePermissions);
+        mEnabled = in.readBoolean();
+        mAvailable = in.readBoolean();
+        mWritable = in.readBoolean();
+        mRestricted = in.readBoolean();
+        mSensitivity = in.readInt();
+        mLaunchIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
+                PendingIntent.class);
+        mExtras = Objects.requireNonNullElseGet(in.readBundle(), Bundle::new);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString8(mKey);
+        dest.writeString8(mScreenKey);
+        dest.writeString8(mTitle);
+        dest.writeString8(mSummary);
+        dest.writeStringList(mBreadcrumbs);
+        dest.writeStringList(mReadPermissions);
+        dest.writeStringList(mWritePermissions);
+        dest.writeBoolean(mEnabled);
+        dest.writeBoolean(mAvailable);
+        dest.writeBoolean(mWritable);
+        dest.writeBoolean(mRestricted);
+        dest.writeInt(mSensitivity);
+        dest.writeParcelable(mLaunchIntent, flags);
+        dest.writeBundle(mExtras);
+    }
+
+    /**
+     * Parcelable Creator for {@link SettingsPreferenceMetadata}.
+     */
+    @NonNull
+    public static final Creator<SettingsPreferenceMetadata> CREATOR = new Creator<>() {
+        @Override
+        public SettingsPreferenceMetadata createFromParcel(@NonNull Parcel in) {
+            return new SettingsPreferenceMetadata(in);
+        }
+
+        @Override
+        public SettingsPreferenceMetadata[] newArray(int size) {
+            return new SettingsPreferenceMetadata[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link SettingsPreferenceMetadata}.
+     */
+    public static final class Builder {
+        private final String mScreenKey;
+        private final String mKey;
+        private String mTitle;
+        private String mSummary;
+        private List<String> mBreadcrumbs = Collections.emptyList();
+        private List<String> mReadPermissions = Collections.emptyList();
+        private List<String> mWritePermissions = Collections.emptyList();
+        private boolean mEnabled = false;
+        private boolean mAvailable = false;
+        private boolean mWritable = false;
+        private boolean mRestricted = false;
+        @WriteSensitivity private int mSensitivity = INTENT_ONLY;
+        private PendingIntent mLaunchIntent;
+        private Bundle mExtras;
+
+        /**
+         * Create Builder instance.
+         * @param screenKey required to be not empty
+         * @param key required to be not empty
+         */
+        public Builder(@NonNull String screenKey, @NonNull String key) {
+            if (TextUtils.isEmpty(screenKey)) {
+                throw new IllegalArgumentException("screenKey cannot be empty");
+            }
+            if (TextUtils.isEmpty(key)) {
+                throw new IllegalArgumentException("key cannot be empty");
+            }
+            mScreenKey = screenKey;
+            mKey = key;
+        }
+
+        /**
+         * Sets the preference title.
+         */
+        @NonNull
+        public Builder setTitle(@Nullable String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the preference summary.
+         */
+        @NonNull
+        public Builder setSummary(@Nullable String summary) {
+            mSummary = summary;
+            return this;
+        }
+
+        /**
+         * Sets the preference breadcrumbs (navigation context).
+         */
+        @NonNull
+        public Builder setBreadcrumbs(@NonNull List<String> breadcrumbs) {
+            mBreadcrumbs = breadcrumbs;
+            return this;
+        }
+
+        /**
+         * Sets the permissions required for reading this preference.
+         */
+        @NonNull
+        public Builder setReadPermissions(@NonNull List<String> readPermissions) {
+            mReadPermissions = readPermissions;
+            return this;
+        }
+
+        /**
+         * Sets the permissions required for writing this preference.
+         */
+        @NonNull
+        public Builder setWritePermissions(@NonNull List<String> writePermissions) {
+            mWritePermissions = writePermissions;
+            return this;
+        }
+
+        /**
+         * Set whether the preference is enabled.
+         */
+        @NonNull
+        public Builder setEnabled(boolean enabled) {
+            mEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Sets whether the preference is available.
+         */
+        @NonNull
+        public Builder setAvailable(boolean available) {
+            mAvailable = available;
+            return this;
+        }
+
+        /**
+         * Sets whether the preference is writable.
+         */
+        @NonNull
+        public Builder setWritable(boolean writable) {
+            mWritable = writable;
+            return this;
+        }
+
+        /**
+         * Sets whether the preference is restricted.
+         */
+        @NonNull
+        public Builder setRestricted(boolean restricted) {
+            mRestricted = restricted;
+            return this;
+        }
+
+        /**
+         * Sets the preference write-level sensitivity.
+         */
+        @NonNull
+        public Builder setWriteSensitivity(@WriteSensitivity int sensitivity) {
+            mSensitivity = sensitivity;
+            return this;
+        }
+
+        /**
+         * Sets the intent to launch the host app page for this preference.
+         */
+        @NonNull
+        public Builder setLaunchIntent(@Nullable PendingIntent launchIntent) {
+            mLaunchIntent = launchIntent;
+            return this;
+        }
+
+        /**
+         * Sets additional fields specific to this preference. Treat all data as optional.
+         */
+        @NonNull
+        public Builder setExtras(@NonNull Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+         * Constructs an immutable {@link SettingsPreferenceMetadata} object.
+         */
+        @NonNull
+        public SettingsPreferenceMetadata build() {
+            return new SettingsPreferenceMetadata(this);
+        }
+    }
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceService.java b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
new file mode 100644
index 0000000..4a4b5d2
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
@@ -0,0 +1,201 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.Manifest;
+import android.annotation.EnforcePermission;
+import android.annotation.FlaggedApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.PermissionEnforcer;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+/**
+ * Base class for a service that exposes its settings preferences to external access.
+ * <p>This class is to be implemented by apps that contribute to the Android Settings surface.
+ * Access to this service is permission guarded by
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} for binding and reading, and guarded by both
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} and
+ * {@link android.permission.WRITE_SYSTEM_PREFERENCES} for writing. An additional checks for access
+ * control are the responsibility of the implementing class.
+ *
+ * <p>This implementation must correspond to an exported service declaration in the host app
+ * AndroidManifest.xml as follows
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ *     android:permission="android.permission.READ_SYSTEM_PREFERENCES"
+ *     android:exported="true">
+ *     <intent-filter>
+ *         <action android:name="android.service.settings.preferences.action.PREFERENCE_SERVICE" />
+ *     </intent-filter>
+ * </service>}
+ * </pre>
+ *
+ * <ul>
+ *   <li>It is recommended to expose the metadata for most, if not all, preferences within a
+ *   settings app, thus implementing {@link #onGetAllPreferenceMetadata}.
+ *   <li>Exposing preferences for read access of their values is up to the implementer, but any
+ *   exposed must be a subset of the preferences exposed in {@link #onGetAllPreferenceMetadata}.
+ *   To expose a preference for read access, the implementation will contain
+ *   {@link #onGetPreferenceValue}.
+ *   <li>Exposing a preference for write access of their values is up to the implementer, but should
+ *   be done so with extra care and consideration, both for security and privacy. These must also
+ *   be a subset of those exposed in {@link #onGetAllPreferenceMetadata}. To expose a preference for
+ *   write access, the implementation will contain {@link #onSetPreferenceValue}.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public abstract class SettingsPreferenceService extends Service {
+
+    /**
+     * Intent Action corresponding to a {@link SettingsPreferenceService}. Note that any checks for
+     * such services must be accompanied by a check to ensure the host is a system application.
+     * Given an {@link android.content.pm.ApplicationInfo} you can check for
+     * {@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}, or when querying
+     * {@link PackageManager#queryIntentServices} you can provide the flag
+     * {@link PackageManager#MATCH_SYSTEM_ONLY}.
+     */
+    public static final String ACTION_PREFERENCE_SERVICE =
+            "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+
+    /** @hide */
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return new ISettingsPreferenceService.Stub(
+                PermissionEnforcer.fromContext(getApplicationContext())) {
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getAllPreferenceMetadata(MetadataRequest request,
+                                                 IMetadataCallback callback) {
+                getAllPreferenceMetadata_enforcePermission();
+                onGetAllPreferenceMetadata(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(MetadataResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getPreferenceValue(GetValueRequest request, IGetValueCallback callback) {
+                getPreferenceValue_enforcePermission();
+                onGetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(GetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(allOf = {
+                    Manifest.permission.READ_SYSTEM_PREFERENCES,
+                    Manifest.permission.WRITE_SYSTEM_PREFERENCES
+            })
+            @Override
+            public void setPreferenceValue(SetValueRequest request, ISetValueCallback callback) {
+                setPreferenceValue_enforcePermission();
+                onSetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(SetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within this application. This
+     * data should be a snapshot of their state at the time of this method being called.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> callback);
+
+    /**
+     * Retrieve the current value of the requested settings preference. If this value is not exposed
+     * or cannot be obtained for some reason, the corresponding result code will be set on the
+     * result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetPreferenceValue(
+            @NonNull GetValueRequest request,
+            @NonNull OutcomeReceiver<GetValueResult, Exception> callback);
+
+    /**
+     * Set the value within the request to the target settings preference. If this value cannot
+     * be written for some reason, the corresponding result code will be set on the result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onSetPreferenceValue(
+            @NonNull SetValueRequest request,
+            @NonNull OutcomeReceiver<SetValueResult, Exception> callback);
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
new file mode 100644
index 0000000..39995a4
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
@@ -0,0 +1,248 @@
+/*
+ * 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.service.settings.preferences;
+
+import static android.service.settings.preferences.SettingsPreferenceService.ACTION_PREFERENCE_SERVICE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Client class responsible for binding to and interacting with an instance of
+ * {@link SettingsPreferenceService}.
+ * <p>This is a convenience class to handle the lifecycle of the service connection.
+ * <p>This client will only interact with one instance at a time,
+ * so if the caller requires multiple instances (multiple applications that provide settings), then
+ * the caller must create multiple client classes, one for each instance required. To find all
+ * available services, a caller may query {@link android.content.pm.PackageManager} for applications
+ * that provide the intent action {@link SettingsPreferenceService#ACTION_PREFERENCE_SERVICE} that
+ * are also system applications ({@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}).
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public class SettingsPreferenceServiceClient implements AutoCloseable {
+
+    private final Context mContext;
+    private final Intent mServiceIntent;
+    private final ServiceConnection mServiceConnection;
+    private final boolean mSystemOnly;
+    private ISettingsPreferenceService mRemoteService;
+
+    /**
+     * Construct a client for binding to a {@link SettingsPreferenceService} provided by the
+     * application corresponding to the provided package name.
+     * @param packageName - package name for which this client will initiate a service binding
+     */
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName) {
+        this(context, packageName, true, null);
+    }
+
+    /**
+     * @hide Only to be called directly by test
+     */
+    @TestApi
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName,
+                                           boolean systemOnly,
+                                           @Nullable ServiceConnection connectionListener) {
+        mContext = context.getApplicationContext();
+        mServiceIntent = new Intent(ACTION_PREFERENCE_SERVICE).setPackage(packageName);
+        mSystemOnly = systemOnly;
+        mServiceConnection = createServiceConnection(connectionListener);
+    }
+
+    /**
+     * Initiate binding to service.
+     * <p>If no service exists for the package provided or the package is not for a system
+     * application, no binding will occur.
+     */
+    public void start() {
+        PackageManager pm = mContext.getPackageManager();
+        PackageManager.ResolveInfoFlags flags;
+        if (mSystemOnly) {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY);
+        } else {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL);
+        }
+        List<ResolveInfo> infos = pm.queryIntentServices(mServiceIntent, flags);
+        if (infos.size() == 1) {
+            mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    /**
+     * If there is an active service binding, unbind from that service.
+     */
+    public void stop() {
+        if (mRemoteService != null) {
+            mRemoteService = null;
+            mContext.unbindService(mServiceConnection);
+        }
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within the application.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getAllPreferenceMetadata(request, new IMetadataCallback.Stub() {
+                @Override
+                public void onSuccess(MetadataResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Retrieve the current value of the requested settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getPreferenceValue(@NonNull GetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<GetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getPreferenceValue(request, new IGetValueCallback.Stub() {
+                @Override
+                public void onSuccess(GetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Set the value on the target settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void setPreferenceValue(@NonNull SetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<SetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.setPreferenceValue(request, new ISetValueCallback.Stub() {
+                @Override
+                public void onSuccess(SetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    @NonNull
+    private ServiceConnection createServiceConnection(@Nullable ServiceConnection listener) {
+        return new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mRemoteService = getPreferenceServiceInterface(service);
+                if (listener != null) {
+                    listener.onServiceConnected(name, service);
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mRemoteService = null;
+                if (listener != null) {
+                    listener.onServiceDisconnected(name);
+                }
+            }
+        };
+    }
+
+    @NonNull
+    private ISettingsPreferenceService getPreferenceServiceInterface(@NonNull IBinder service) {
+        return ISettingsPreferenceService.Stub.asInterface(service);
+    }
+
+    /**
+     * This client handles a resource, thus is it important to appropriately close that resource
+     * when it is no longer needed.
+     * <p>This method is provided by {@link AutoCloseable} and calling it
+     * will unbind any service binding.
+     */
+    @Override
+    public void close() {
+        stop();
+    }
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceValue.java b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java
new file mode 100644
index 0000000..f056e34
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java
@@ -0,0 +1,220 @@
+/*
+ * 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.service.settings.preferences;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This objects represents a value that can be used for a particular settings preference.
+ * <p>The data type for the value will correspond to {@link #getType}. For possible types, see
+ * constants below, such as {@link #TYPE_BOOLEAN} and {@link #TYPE_STRING}.
+ * Depending on the type, the corresponding getter will contain its value. All other getters will
+ * return default values (boolean returns false, String returns null) so they should not be used.
+ * <p>See documentation on the constants for which getter method should be used.
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public final class SettingsPreferenceValue implements Parcelable {
+
+    @Type
+    private final int mType;
+    private final boolean mBooleanValue;
+    private final long mLongValue;
+    private final double mDoubleValue;
+    @Nullable
+    private final String mStringValue;
+
+    /**
+     * Returns the type indicator for Preference value.
+     */
+    @Type
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the boolean value for Preference if type is {@link #TYPE_BOOLEAN}.
+     */
+    public boolean getBooleanValue() {
+        return mBooleanValue;
+    }
+
+    /**
+     * Returns the long value for Preference if type is {@link #TYPE_LONG}.
+     */
+    public long getLongValue() {
+        return mLongValue;
+    }
+
+    /**
+     * Returns the double value for Preference if type is {@link #TYPE_DOUBLE}.
+     */
+    public double getDoubleValue() {
+        return mDoubleValue;
+    }
+
+    /**
+     * Returns the string value for Preference if type is {@link #TYPE_STRING}.
+     */
+    @Nullable
+    public String getStringValue() {
+        return mStringValue;
+    }
+
+    /** @hide */
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_BOOLEAN,
+            TYPE_LONG,
+            TYPE_DOUBLE,
+            TYPE_STRING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
+    /** Value is of type boolean. Access via {@link #getBooleanValue}. */
+    public static final int TYPE_BOOLEAN = 0;
+    /** Value is of type long. Access via {@link #getLongValue()}. */
+    public static final int TYPE_LONG = 1;
+    /** Value is of type double. Access via {@link #getDoubleValue()}. */
+    public static final int TYPE_DOUBLE = 2;
+    /** Value is of type string. Access via {@link #getStringValue}. */
+    public static final int TYPE_STRING = 3;
+
+    private SettingsPreferenceValue(@NonNull Builder builder) {
+        mType = builder.mType;
+        mBooleanValue = builder.mBooleanValue;
+        mLongValue = builder.mLongValue;
+        mDoubleValue = builder.mDoubleValue;
+        mStringValue = builder.mStringValue;
+    }
+
+    private SettingsPreferenceValue(@NonNull Parcel in) {
+        mType = in.readInt();
+        mBooleanValue = in.readBoolean();
+        mLongValue = in.readLong();
+        mDoubleValue = in.readDouble();
+        mStringValue = in.readString8();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mType);
+        dest.writeBoolean(mBooleanValue);
+        dest.writeLong(mLongValue);
+        dest.writeDouble(mDoubleValue);
+        dest.writeString8(mStringValue);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Parcelable Creator for {@link SettingsPreferenceValue}.
+     */
+    @NonNull
+    public static final Creator<SettingsPreferenceValue> CREATOR = new Creator<>() {
+        @Override
+        public SettingsPreferenceValue createFromParcel(@NonNull Parcel in) {
+            return new SettingsPreferenceValue(in);
+        }
+
+        @Override
+        public SettingsPreferenceValue[] newArray(int size) {
+            return new SettingsPreferenceValue[size];
+        }
+    };
+
+    /**
+     * Builder to construct {@link SettingsPreferenceValue}.
+     */
+    public static final class Builder {
+        @Type
+        private final int mType;
+        private boolean mBooleanValue;
+        private long mLongValue;
+        private double mDoubleValue;
+        private String mStringValue;
+
+        /**
+         * Create Builder instance.
+         * @param type type indicator for preference value
+         */
+        public Builder(@Type int type) {
+            mType = type;
+        }
+
+        /**
+         * Sets boolean value for Preference.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder setBooleanValue(boolean booleanValue) {
+            mBooleanValue = booleanValue;
+            return this;
+        }
+
+        /**
+         * Sets long value for Preference.
+         */
+        @NonNull
+        public Builder setLongValue(long longValue) {
+            mLongValue = longValue;
+            return this;
+        }
+
+        /**
+         * Sets floating point value for Preference.
+         */
+        @NonNull
+        public Builder setDoubleValue(double doubleValue) {
+            mDoubleValue = doubleValue;
+            return this;
+        }
+
+        /**
+         * Sets string value for Preference.
+         */
+        @NonNull
+        public Builder setStringValue(@Nullable String stringValue) {
+            mStringValue = stringValue;
+            return this;
+        }
+
+        /**
+         * Constructs an immutable {@link SettingsPreferenceValue} object.
+         */
+        @NonNull
+        public SettingsPreferenceValue build() {
+            return new SettingsPreferenceValue(this);
+        }
+    }
+}
diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl
index f76e6ce..bcdd477 100644
--- a/core/java/android/service/wallpaper/IWallpaperService.aidl
+++ b/core/java/android/service/wallpaper/IWallpaperService.aidl
@@ -28,6 +28,6 @@
     void attach(IWallpaperConnection connection,
             IBinder windowToken, int windowType, boolean isPreview,
             int reqWidth, int reqHeight, in Rect padding, int displayId, int which,
-            in WallpaperInfo info, in @nullable WallpaperDescription description);
+            in WallpaperInfo info, in WallpaperDescription description);
     void detach(IBinder windowToken);
 }
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 131fdc8..2061aba 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -17,6 +17,7 @@
 package android.service.wallpaper;
 
 import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING;
+import static android.app.Flags.liveWallpaperContentHandling;
 import static android.app.WallpaperManager.COMMAND_FREEZE;
 import static android.app.WallpaperManager.COMMAND_UNFREEZE;
 import static android.app.WallpaperManager.SetWallpaperFlags;
@@ -2624,7 +2625,7 @@
         private void doAttachEngine() {
             Trace.beginSection("WPMS.onCreateEngine");
             Engine engine;
-            if (mDescription != null) {
+            if (liveWallpaperContentHandling()) {
                 engine = onCreateEngine(mDescription);
             } else {
                 engine = onCreateEngine();
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index bce51f2..1df3b43 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -37,6 +37,7 @@
 import android.telephony.emergency.EmergencyNumber;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.MediaQualityStatus;
+import android.telephony.satellite.NtnSignalStrength;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.IPhoneStateListener;
@@ -1706,6 +1707,11 @@
                 @NetworkRegistrationInfo.ServiceType int[] availableServices) {
             // not supported on the deprecated interface - Use TelephonyCallback instead
         }
+
+        public final void onCarrierRoamingNtnSignalStrengthChanged(
+                @NonNull NtnSignalStrength ntnSignalStrength) {
+            // not supported on the deprecated interface - Use TelephonyCallback instead
+        }
     }
 
     private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index 64a5533..0d1dc46 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -30,6 +30,7 @@
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.MediaQualityStatus;
 import android.telephony.ims.MediaThreshold;
+import android.telephony.satellite.NtnSignalStrength;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -695,6 +696,15 @@
     public static final int EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED = 44;
 
     /**
+     * Event for listening to carrier roaming non-terrestrial network signal strength changes.
+     *
+     * @see CarrierRoamingNtnModeListener
+     *
+     * @hide
+     */
+    public static final int EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED = 45;
+
+    /**
      * @hide
      */
     @IntDef(prefix = {"EVENT_"}, value = {
@@ -741,7 +751,8 @@
             EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED,
             EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED,
             EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED,
-            EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED
+            EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED,
+            EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface TelephonyEvent {
@@ -1805,6 +1816,14 @@
          */
         default void onCarrierRoamingNtnAvailableServicesChanged(
                 @NetworkRegistrationInfo.ServiceType List<Integer> availableServices) {}
+
+        /**
+         * Callback invoked when carrier roaming non-terrestrial network signal strength changes.
+         *
+         * @param ntnSignalStrength non-terrestrial network signal strength.
+         */
+        default void onCarrierRoamingNtnSignalStrengthChanged(
+                @NonNull NtnSignalStrength ntnSignalStrength) {}
     }
 
     /**
@@ -2270,5 +2289,18 @@
             Binder.withCleanCallingIdentity(() -> mExecutor.execute(
                     () -> listener.onCarrierRoamingNtnAvailableServicesChanged(ServiceList)));
         }
+
+        public void onCarrierRoamingNtnSignalStrengthChanged(
+                @NonNull NtnSignalStrength ntnSignalStrength) {
+            if (!Flags.carrierRoamingNbIotNtn()) return;
+
+            CarrierRoamingNtnModeListener listener =
+                    (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get();
+            if (listener == null) return;
+
+            Binder.withCleanCallingIdentity(() -> mExecutor.execute(
+                    () -> listener.onCarrierRoamingNtnSignalStrengthChanged(ntnSignalStrength)));
+
+        }
     }
 }
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 1dab2cf..90b0bb3 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -47,6 +47,7 @@
 import android.telephony.ims.ImsCallSession;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.MediaQualityStatus;
+import android.telephony.satellite.NtnSignalStrength;
 import android.telephony.satellite.SatelliteStateChangeListener;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -1137,6 +1138,23 @@
     }
 
     /**
+     * Notify external listeners that carrier roaming non-terrestrial network
+     * signal strength changed.
+     * @param subId subscription ID.
+     * @param ntnSignalStrength non-terrestrial network signal strength.
+     * @hide
+     */
+    public final void notifyCarrierRoamingNtnSignalStrengthChanged(int subId,
+            @NonNull NtnSignalStrength ntnSignalStrength) {
+        try {
+            sRegistry.notifyCarrierRoamingNtnSignalStrengthChanged(subId, ntnSignalStrength);
+        } catch (RemoteException ex) {
+            // system server crash
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Processes potential event changes from the provided {@link TelephonyCallback}.
      *
      * @param telephonyCallback callback for monitoring callback changes to the telephony state.
@@ -1293,6 +1311,7 @@
             eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED);
             eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED);
             eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED);
+            eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED);
         }
         return eventList;
     }
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 02923ed..f43f172 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -163,10 +163,12 @@
 }
 
 flag {
-  name: "typeface_redesign"
+  name: "typeface_redesign_readonly"
   namespace: "text"
   description: "Decouple variation settings, weight and style information from Typeface class"
   bug: "361260253"
+  # This feature does not support runtime flag switch which leads crash in System UI.
+  is_fixed_read_only: true
 }
 
 flag {
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 8f112f3..4ff04d5 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -447,7 +447,6 @@
                 && Objects.equals(displayCutout, other.displayCutout)
                 && rotation == other.rotation
                 && modeId == other.modeId
-                && renderFrameRate == other.renderFrameRate
                 && hasArrSupport == other.hasArrSupport
                 && Objects.equals(frameRateCategoryRate, other.frameRateCategoryRate)
                 && defaultModeId == other.defaultModeId
@@ -705,6 +704,9 @@
         if (refreshRateOverride > 0) {
             return refreshRateOverride;
         }
+        if (renderFrameRate > 0) {
+            return renderFrameRate;
+        }
         if (supportedModes.length == 0) {
             return 0;
         }
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index dddc408..38e4e27 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -935,7 +935,6 @@
      */
     public static final int KEYCODE_MACRO_4 = 316;
     /** Key code constant: To open emoji picker */
-    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
     public static final int KEYCODE_EMOJI_PICKER = 317;
     /**
      * Key code constant: To take a screenshot
@@ -944,15 +943,80 @@
      * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app
      * doesn't handle it, the framework handles it (to take a screenshot).
      */
-    @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
     public static final int KEYCODE_SCREENSHOT = 318;
+    /** Key code constant: To start dictate to an input field */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_DICTATE = 319;
+    /**
+     * Key code constant: AC New
+     *
+     * e.g. To create a new instance of a window, open a new tab, etc.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_NEW = 320;
+    /**
+     * Key code constant: AC Close
+     *
+     * e.g. To close current instance of the application window, close the current tab, etc.
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_CLOSE = 321;
+    /** Key code constant: To toggle 'Do Not Disturb' mode */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_DO_NOT_DISTURB = 322;
+    /** Key code constant: To Print */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_PRINT = 323;
+    /** Key code constant: To Lock the screen */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_LOCK = 324;
+    /** Key code constant: To toggle fullscreen mode (on the current application) */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_FULLSCREEN = 325;
+    /** Key code constant: F13 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F13 = 326;
+    /** Key code constant: F14 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F14 = 327;
+    /** Key code constant: F15 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F15 = 328;
+    /** Key code constant: F16 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F16 = 329;
+    /** Key code constant: F17 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F17 = 330;
+    /** Key code constant: F18 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F18 = 331;
+    /** Key code constant: F19 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F19 = 332;
+    /** Key code constant: F20 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F20 = 333;
+    /** Key code constant: F21 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F21 = 334;
+    /** Key code constant: F22 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F22 = 335;
+    /** Key code constant: F23 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F23 = 336;
+    /** Key code constant: F24 key. */
+    @FlaggedApi(Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
+    public static final int KEYCODE_F24 = 337;
 
     /**
      * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
      * @hide
      */
     @TestApi
-    public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT;
+    @SuppressWarnings("FlaggedApi")
+    public static final int LAST_KEYCODE = KEYCODE_F24;
 
     /** @hide */
     @IntDef(prefix = {"KEYCODE_"}, value = {
@@ -1275,6 +1339,25 @@
             KEYCODE_MACRO_4,
             KEYCODE_EMOJI_PICKER,
             KEYCODE_SCREENSHOT,
+            KEYCODE_DICTATE,
+            KEYCODE_NEW,
+            KEYCODE_CLOSE,
+            KEYCODE_DO_NOT_DISTURB,
+            KEYCODE_PRINT,
+            KEYCODE_LOCK,
+            KEYCODE_FULLSCREEN,
+            KEYCODE_F13,
+            KEYCODE_F14,
+            KEYCODE_F15,
+            KEYCODE_F16,
+            KEYCODE_F17,
+            KEYCODE_F18,
+            KEYCODE_F19,
+            KEYCODE_F20,
+            KEYCODE_F21,
+            KEYCODE_F22,
+            KEYCODE_F23,
+            KEYCODE_F24,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface KeyCode {}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index df54d31..206c737 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2989,7 +2989,6 @@
         private void apply(boolean sync, boolean oneWay) {
             applyResizedSurfaces();
             notifyReparentedSurfaces();
-            nativeApplyTransaction(mNativeObject, sync, oneWay);
 
             if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
                 SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
@@ -2998,6 +2997,7 @@
             if (mCalls != null) {
                 mCalls.clear();
             }
+            nativeApplyTransaction(mNativeObject, sync, oneWay);
         }
 
         /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 618843c..fa06831 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -23882,12 +23882,12 @@
                     } else {
                         draw(canvas);
                     }
-                }
 
-                // For VRR to vote the preferred frame rate
-                if (sToolkitSetFrameRateReadOnlyFlagValue
-                        && sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
-                    votePreferredFrameRate();
+                    // For VRR to vote the preferred frame rate
+                    if (sToolkitSetFrameRateReadOnlyFlagValue
+                            && sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
+                        votePreferredFrameRate();
+                    }
                 }
             } finally {
                 renderNode.endRecording();
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9a2aa0b..75d2da1 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -9951,11 +9951,13 @@
             return false;
         }
 
-        if (!mIsDrawing) {
-            destroyHardwareRenderer();
-        } else {
-            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
-                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
+        if (!com.android.graphics.hwui.flags.Flags.removeVriSketchyDestroy()) {
+            if (!mIsDrawing) {
+                destroyHardwareRenderer();
+            } else {
+                Log.e(mTag, "Attempting to destroy the window while drawing!\n"
+                        + "  window=" + this + ", title=" + mWindowAttributes.getTitle());
+            }
         }
         mHandler.sendEmptyMessage(MSG_DIE);
         return true;
@@ -9976,9 +9978,9 @@
                 dispatchDetachedFromWindow();
             }
 
-            if (mAdded && !mFirst) {
-                destroyHardwareRenderer();
+            destroyHardwareRenderer();
 
+            if (mAdded && !mFirst) {
                 if (mView != null) {
                     int viewVisibility = mView.getVisibility();
                     boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 14652035..0204517 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -3866,8 +3866,14 @@
      * Sets the view for which the view represented by this info serves as a
      * label for accessibility purposes.
      *
+     * @deprecated Use {@link #addLabeledBy(View)} on the labeled node instead,
+     * since {@link #getLabeledByList()} and {@link #getLabeledBy()} on the
+     * labeled node are not automatically populated when this method is used.
+     *
      * @param labeled The view for which this info serves as a label.
      */
+    @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS)
+    @Deprecated
     public void setLabelFor(View labeled) {
         setLabelFor(labeled, AccessibilityNodeProvider.HOST_VIEW_ID);
     }
@@ -3888,9 +3894,15 @@
      *   This class is made immutable before being delivered to an AccessibilityService.
      * </p>
      *
+     * @deprecated Use {@link #addLabeledBy(View)} on the labeled node instead,
+     * since {@link #getLabeledByList()} and {@link #getLabeledBy()} on the
+     * labeled node are not automatically populated when this method is used.
+     *
      * @param root The root whose virtual descendant serves as a label.
      * @param virtualDescendantId The id of the virtual descendant.
      */
+    @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS)
+    @Deprecated
     public void setLabelFor(View root, int virtualDescendantId) {
         enforceNotSealed();
         final int rootAccessibilityViewId = (root != null)
@@ -3902,8 +3914,14 @@
      * Gets the node info for which the view represented by this info serves as
      * a label for accessibility purposes.
      *
+     * @deprecated Use {@link #getLabeledByList()} on the labeled node instead,
+     * since calling {@link #addLabeledBy(View)} or {@link #addLabeledBy(View, int)}
+     * on the labeled node do not automatically provide that node from this method.
+     *
      * @return The labeled info.
      */
+    @FlaggedApi(Flags.FLAG_DEPRECATE_ANI_LABEL_FOR_APIS)
+    @Deprecated
     public AccessibilityNodeInfo getLabelFor() {
         enforceSealed();
         return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabelForId);
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 7177ef3..8a006fa 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -92,6 +92,13 @@
 
 flag {
     namespace: "accessibility"
+    name: "deprecate_ani_label_for_apis"
+    description: "Controls the deprecation of AccessibilityNodeInfo labelFor apis"
+    bug: "333783827"
+}
+
+flag {
+    namespace: "accessibility"
     name: "fix_merged_content_change_event_v2"
     description: "Fixes event type and source of content change event merged in ViewRootImpl"
     bug: "277305460"
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index 0ab51e4..905f350 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -316,6 +316,35 @@
 
     // END AUTOFILL PCC CLASSIFICATION FLAGS
 
+    // START AUTOFILL REMOVE PRE_TRIGGER FLAGS
+
+    /**
+     * Whether pre-trigger flow is disabled.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED = "improve_fill_dialog";
+
+    /**
+     * Minimum amount of time (in milliseconds) to wait after IME animation finishes, and before
+     * starting fill dialog animation.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS =
+            "fill_dialog_min_wait_after_animation_end_ms";
+
+    /**
+     * Sets a value of timeout in milliseconds, measured after animation end, during which fill
+     * dialog can be shown. If we are at time > animation_end_time + this timeout, fill dialog
+     * wouldn't be shown.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS = "fill_dialog_timeout_ms";
+
+    // END AUTOFILL REMOVE PRE_TRIGGER FLAGS
+
     /**
      * Define the max input length for autofill to show suggesiton UI
      *
@@ -366,6 +395,17 @@
             DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = true;
     // END AUTOFILL FOR ALL APPS DEFAULTS
 
+    // START AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS
+    // Default for whether the pre trigger removal is enabled.
+    /** @hide */
+    public static final boolean DEFAULT_IMPROVE_FILL_DIALOG_ENABLED = true;
+    // Default for whether the pre trigger removal is enabled.
+    /** @hide */
+    public static final long DEFAULT_FILL_DIALOG_TIMEOUT_MS = 300; // 300 ms
+    /** @hide */
+    public static final long DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = 0; // 0 ms
+    // END AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS
+
     /**
      * @hide
      */
@@ -611,4 +651,48 @@
     }
 
     // END AUTOFILL PCC CLASSIFICATION FUNCTIONS
+
+
+    // START AUTOFILL REMOVE PRE_TRIGGER
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static boolean isImproveFillDialogEnabled() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED,
+                DEFAULT_IMPROVE_FILL_DIALOG_ENABLED);
+    }
+
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static long getFillDialogTimeoutMs() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS,
+                DEFAULT_FILL_DIALOG_TIMEOUT_MS);
+    }
+
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static long getFillDialogMinWaitAfterImeAnimationtEndMs() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS,
+                DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS);
+    }
 }
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index aa4927e..edd9d6c 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -158,3 +158,11 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+    name: "writing_tools"
+    namespace: "input_method"
+    description: "Writing tools API"
+    bug: "373788889"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index dae87dd..6b5a367 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -75,7 +75,8 @@
             Flags::enableDesktopAppLaunchAlttabTransitions, false),
     ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS(
             Flags::enableDesktopAppLaunchTransitions, false),
-    ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false);
+    ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false),
+    ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true);
 
     private static final String TAG = "DesktopModeFlagsUtil";
     // Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d39ecab..f474b34 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -353,6 +353,16 @@
 }
 
 flag {
+    name: "enable_desktop_system_dialogs_transitions"
+    namespace: "lse_desktop_experience"
+    description: "Enables custom transitions for system dialogs in Desktop Mode."
+    bug: "335638193"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_move_to_next_display_shortcut"
     namespace: "lse_desktop_experience"
     description: "Add new keyboard shortcut of moving a task into next display"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 9845bf0..d9de38a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -76,6 +76,14 @@
 }
 
 flag {
+  name: "disable_opt_out_edge_to_edge"
+  namespace: "windowing_frontend"
+  description: "Deprecate and disable windowOptOutEdgeToEdgeEnforcement"
+  bug: "377864165"
+  is_fixed_read_only: true
+}
+
+flag {
   name: "keyguard_going_away_timeout"
   namespace: "windowing_frontend"
   description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting"
@@ -260,6 +268,16 @@
 }
 
 flag {
+  name: "system_ui_post_animation_end"
+  namespace: "windowing_frontend"
+  description: "Run AnimatorListener#onAnimationEnd on next frame for SystemUI"
+  bug: "300035126"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "system_ui_immersive_confirmation_dialog"
   namespace: "windowing_frontend"
   description: "Enable the implementation of the immersive confirmation dialog on system UI side by default"
diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
index 44dceb9..4a49bb6 100644
--- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
+++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
@@ -63,6 +63,8 @@
      * quickly tapping screen 2 times with two fingers as preferred shortcut.
      * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which
      * choose Quick Settings as preferred shortcut.
+     * {@code KEY_GESTURE} for shortcuts which are directly from key gestures and should be
+     * activated always.
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
@@ -73,6 +75,7 @@
             UserShortcutType.TWOFINGER_DOUBLETAP,
             UserShortcutType.QUICK_SETTINGS,
             UserShortcutType.GESTURE,
+            UserShortcutType.KEY_GESTURE,
             UserShortcutType.ALL
     })
     public @interface UserShortcutType {
@@ -84,8 +87,10 @@
         int TWOFINGER_DOUBLETAP = 1 << 3;
         int QUICK_SETTINGS = 1 << 4;
         int GESTURE = 1 << 5;
+        int KEY_GESTURE = 1 << 6;
         // LINT.ThenChange(:shortcut_type_array)
-        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE;
+        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE
+                | KEY_GESTURE;
     }
 
     /**
@@ -99,7 +104,8 @@
             UserShortcutType.TRIPLETAP,
             UserShortcutType.TWOFINGER_DOUBLETAP,
             UserShortcutType.QUICK_SETTINGS,
-            UserShortcutType.GESTURE
+            UserShortcutType.GESTURE,
+            UserShortcutType.KEY_GESTURE
             // LINT.ThenChange(:shortcut_type_intdef)
     };
 
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index 2e0ff3d..14ca0f8 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -27,6 +27,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -187,6 +188,7 @@
             case TWOFINGER_DOUBLETAP ->
                     Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED;
             case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS;
+            case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS;
             default -> throw new IllegalArgumentException(
                     "Unsupported user shortcut type: " + type);
         };
@@ -209,6 +211,7 @@
                     TRIPLETAP;
             case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
                     TWOFINGER_DOUBLETAP;
+            case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE;
             default -> throw new IllegalArgumentException(
                     "Unsupported user shortcut key: " + key);
         };
diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
index b3480ab..2931bd2 100644
--- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
@@ -56,7 +56,7 @@
  * @hide
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("LongArrayMultiStateCounter_host")
+@RavenwoodRedirectionClass("LongArrayMultiStateCounter_ravenwood")
 public final class LongArrayMultiStateCounter implements Parcelable {
     private static volatile NativeAllocationRegistry sRegistry;
     private final int mStateCount;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java
similarity index 98%
rename from ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
rename to core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java
index 90608f6..7030d8e 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
+++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter_ravenwood.java
@@ -18,6 +18,7 @@
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -25,7 +26,8 @@
 /**
  * Native implementation substitutions for the LongArrayMultiStateCounter class.
  */
-public class LongArrayMultiStateCounter_host {
+@RavenwoodKeepWholeClass
+class LongArrayMultiStateCounter_ravenwood {
 
     /**
      * A reimplementation of {@link LongArrayMultiStateCounter}, only in
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
index c386a86..ee855d5 100644
--- a/core/java/com/android/internal/os/LongMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -60,7 +60,7 @@
  * @hide
  */
 @RavenwoodKeepWholeClass
-@RavenwoodRedirectionClass("LongMultiStateCounter_host")
+@RavenwoodRedirectionClass("LongMultiStateCounter_ravenwood")
 public final class LongMultiStateCounter implements Parcelable {
 
     private static NativeAllocationRegistry sRegistry;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java b/core/java/com/android/internal/os/LongMultiStateCounter_ravenwood.java
similarity index 97%
rename from ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
rename to core/java/com/android/internal/os/LongMultiStateCounter_ravenwood.java
index 1d95aa1..42db37f 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter_ravenwood.java
@@ -18,13 +18,15 @@
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
 
 import java.util.HashMap;
 
 /**
  * Native implementation substitutions for the LongMultiStateCounter class.
  */
-public class LongMultiStateCounter_host {
+@RavenwoodKeepWholeClass
+class LongMultiStateCounter_ravenwood {
 
     /**
      * A reimplementation of {@link com.android.internal.os.LongMultiStateCounter}, only in
diff --git a/core/java/com/android/internal/statusbar/StatusBarIcon.java b/core/java/com/android/internal/statusbar/StatusBarIcon.java
index 1938cdb..40161023 100644
--- a/core/java/com/android/internal/statusbar/StatusBarIcon.java
+++ b/core/java/com/android/internal/statusbar/StatusBarIcon.java
@@ -40,9 +40,6 @@
     public enum Type {
         // Notification: the sender avatar for important conversations
         PeopleAvatar,
-        // Notification: the monochrome version of the app icon if available; otherwise fall back to
-        // the small icon
-        MaybeMonochromeAppIcon,
         // Notification: the small icon from the notification
         NotifSmallIcon,
         // The wi-fi, cellular or battery icon.
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index b5c87868..0e85e04 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -27,6 +27,7 @@
 import android.telephony.PhysicalChannelConfig;
 import android.telephony.PreciseCallState;
 import android.telephony.PreciseDataConnectionState;
+import android.telephony.satellite.NtnSignalStrength;
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
 import android.telephony.emergency.EmergencyNumber;
@@ -85,4 +86,5 @@
     void onCarrierRoamingNtnModeChanged(in boolean active);
     void onCarrierRoamingNtnEligibleStateChanged(in boolean eligible);
     void onCarrierRoamingNtnAvailableServicesChanged(in int[] availableServices);
+    void onCarrierRoamingNtnSignalStrengthChanged(in NtnSignalStrength ntnSignalStrength);
 }
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 1c76a6c..0f268d5d 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -29,6 +29,7 @@
 import android.telephony.PhoneCapability;
 import android.telephony.PhysicalChannelConfig;
 import android.telephony.PreciseDataConnectionState;
+import android.telephony.satellite.NtnSignalStrength;
 import android.telephony.ServiceState;
 import android.telephony.SignalStrength;
 import android.telephony.emergency.EmergencyNumber;
@@ -125,8 +126,10 @@
     void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active);
     void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible);
     void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices);
+    void notifyCarrierRoamingNtnSignalStrengthChanged(int subId, in NtnSignalStrength ntnSignalStrength);
 
     void addSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg, String featureId);
     void removeSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg);
     void notifySatelliteStateChanged(boolean isEnabled);
+
 }
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 30deb49..fb6937c 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -63,6 +63,7 @@
 
     private final ArrayList<Part> mParts = new ArrayList<>();
 
+    private final RectF mSegRectF = new RectF();
     private final Rect mPointRect = new Rect();
     private final RectF mPointRectF = new RectF();
 
@@ -198,22 +199,42 @@
                         mState.mSegSegGap, x + segWidth, totalWidth);
                 final float end = x + segWidth - endOffset;
 
-                // Transparent is not allowed (and also is the default in the data), so use that
-                // as a sentinel to be replaced by default
-                mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
-                        : mState.mStrokeColor);
-                mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
-                        : mState.mFadedStrokeColor);
-
-                // Leave space for the rounded line cap which extends beyond start/end.
-                final float capWidth = mStrokePaint.getStrokeWidth() / 2F;
-
-                canvas.drawLine(start + capWidth, centerY, end - capWidth, centerY,
-                        segment.mDashed ? mDashedStrokePaint : mStrokePaint);
-
                 // Advance the current position to account for the segment's fraction of the total
                 // width (ignoring offset and padding)
                 x += segWidth;
+
+                // No space left to draw the segment
+                if (start > end) continue;
+
+                if (segment.mDashed) {
+                    // No caps when the segment is dashed.
+
+                    mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
+                            : mState.mFadedStrokeColor);
+                    canvas.drawLine(start, centerY, end, centerY, mDashedStrokePaint);
+                } else if (end - start < mState.mStrokeWidth) {
+                    // Not enough segment length to draw the caps
+
+                    final float rad = (end - start) / 2F;
+                    final float capWidth = mStrokePaint.getStrokeWidth() / 2F;
+
+                    mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
+                            : mState.mStrokeColor);
+
+                    mSegRectF.set(start, centerY - capWidth, end, centerY + capWidth);
+                    canvas.drawRoundRect(mSegRectF, rad, rad, mFillPaint);
+                } else {
+                    // Leave space for the rounded line cap which extends beyond start/end.
+                    final float capWidth = mStrokePaint.getStrokeWidth() / 2F;
+
+                    // Transparent is not allowed (and also is the default in the data), so use that
+                    // as a sentinel to be replaced by default
+                    mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
+                            : mState.mStrokeColor);
+
+                    canvas.drawLine(start + capWidth, centerY, end - capWidth, centerY,
+                            mStrokePaint);
+                }
             } else if (part instanceof Point point) {
                 final float pointWidth = 2 * pointRadius;
                 float start = x - pointRadius;
@@ -232,7 +253,7 @@
                 } else {
                     // TODO: b/367804171 - actually use a vector asset for the default point
                     //  rather than drawing it as a box?
-                    mPointRectF.set(mPointRect);
+                    mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius);
                     final float inset = mState.mPointRectInset;
                     final float cornerRadius = mState.mPointRectCornerRadius;
                     mPointRectF.inset(inset, inset);
diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java
index adcc0f6..5fc61b0 100644
--- a/core/java/com/android/internal/widget/NotificationRowIconView.java
+++ b/core/java/com/android/internal/widget/NotificationRowIconView.java
@@ -22,11 +22,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
 import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
@@ -35,8 +31,6 @@
 import android.view.RemotableViewMethod;
 import android.widget.RemoteViews;
 
-import com.android.internal.R;
-
 /**
  * An image view that holds the icon displayed at the start of a notification row.
  * This can generally either display the "small icon" of a notification set via
@@ -48,7 +42,6 @@
     private NotificationIconProvider mIconProvider;
 
     private boolean mApplyCircularCrop = false;
-    private boolean mShouldShowAppIcon = false;
     private Drawable mAppIcon = null;
 
     // Padding, background and colors set on the view prior to being overridden when showing the app
@@ -77,17 +70,6 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
-    @Override
-    protected void onFinishInflate() {
-        // If showing the app icon, we don't need background or padding.
-        if (Flags.notificationsUseAppIcon()) {
-            setPadding(0, 0, 0, 0);
-            setBackground(null);
-        }
-
-        super.onFinishInflate();
-    }
-
     /**
      * Sets the icon provider for this view. This is used to determine whether we should show the
      * app icon instead of the small icon, and to fetch the app icon if needed.
@@ -153,37 +135,12 @@
         return super.setImageIconAsync(icon);
     }
 
-    /** Whether the icon represents the app icon (instead of the small icon). */
-    @RemotableViewMethod
-    public void setShouldShowAppIcon(boolean shouldShowAppIcon) {
-        if (Flags.notificationsUseAppIconInRow()) {
-            if (mShouldShowAppIcon == shouldShowAppIcon) {
-                return; // no change
-            }
-
-            mShouldShowAppIcon = shouldShowAppIcon;
-            if (mShouldShowAppIcon) {
-                adjustViewForAppIcon();
-            } else {
-                // Restore original padding and background if needed
-                restoreViewForSmallIcon();
-            }
-        }
-    }
-
     /**
      * Override padding and background from the view to display the app icon.
      */
     private void adjustViewForAppIcon() {
         removePadding();
-
-        if (Flags.notificationsUseAppIconInRow()) {
-            addWhiteBackground();
-        } else {
-            // No need to set the background for notification redesign, since the icon
-            // factory already does that for us.
-            removeBackground();
-        }
+        removeBackground();
     }
 
     /**
@@ -221,21 +178,6 @@
         setBackground(null);
     }
 
-    private void addWhiteBackground() {
-        if (mOriginalBackground == null) {
-            mOriginalBackground = getBackground();
-        }
-
-        // Make the background white in case the icon itself doesn't have one.
-        ColorFilter colorFilter = new PorterDuffColorFilter(Color.WHITE,
-                PorterDuff.Mode.SRC_ATOP);
-
-        if (mOriginalBackground == null) {
-            setBackground(getContext().getDrawable(R.drawable.notification_icon_circle));
-        }
-        getBackground().mutate().setColorFilter(colorFilter);
-    }
-
     private void restoreBackground() {
         // NOTE: This will not work if the original background was null, but that's better than
         //  accidentally clearing the background. We expect that there's generally going to be one
diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp
index 50252c1..4240614 100644
--- a/core/jni/android_hardware_Camera.cpp
+++ b/core/jni/android_hardware_Camera.cpp
@@ -538,7 +538,7 @@
         return false;
     }
 
-    if (!(useContextAttributionSource && flags::use_context_attribution_source())) {
+    if (!(useContextAttributionSource && flags::data_delivery_permission_checks())) {
         clientAttribution.uid = Camera::USE_CALLING_UID;
         clientAttribution.pid = Camera::USE_CALLING_PID;
     }
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index 69f6334..f1c4913 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -83,68 +83,53 @@
     jmethodID ctor;
 } gRegionClassInfo;
 
-static Mutex gHandleMutex;
+// --- Global functions ---
 
+sp<gui::WindowInfoHandle> android_view_InputWindowHandle_getHandle(JNIEnv* env, jobject obj) {
+    sp<gui::WindowInfoHandle> handle = [&]() {
+        jlong cachedHandle = env->GetLongField(obj, gInputWindowHandleClassInfo.ptr);
+        if (cachedHandle) {
+            return sp<gui::WindowInfoHandle>::fromExisting(
+                    reinterpret_cast<gui::WindowInfoHandle*>(cachedHandle));
+        }
 
-// --- NativeInputWindowHandle ---
+        auto newHandle = sp<gui::WindowInfoHandle>::make();
+        newHandle->incStrong((void*)android_view_InputWindowHandle_getHandle);
+        env->SetLongField(obj, gInputWindowHandleClassInfo.ptr,
+                          reinterpret_cast<jlong>(newHandle.get()));
+        return newHandle;
+    }();
 
-NativeInputWindowHandle::NativeInputWindowHandle(jweak objWeak) :
-        mObjWeak(objWeak) {
-}
+    gui::WindowInfo* windowInfo = handle->editInfo();
 
-NativeInputWindowHandle::~NativeInputWindowHandle() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->DeleteWeakGlobalRef(mObjWeak);
-
-    // Clear the weak reference to the layer handle and flush any binder ref count operations so we
-    // do not hold on to any binder references.
-    // TODO(b/139697085) remove this after it can be flushed automatically
-    mInfo.touchableRegionCropHandle.clear();
-    IPCThreadState::self()->flushCommands();
-}
-
-jobject NativeInputWindowHandle::getInputWindowHandleObjLocalRef(JNIEnv* env) {
-    return env->NewLocalRef(mObjWeak);
-}
-
-bool NativeInputWindowHandle::updateInfo() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jobject obj = env->NewLocalRef(mObjWeak);
-    if (!obj) {
-        releaseChannel();
-        return false;
-    }
-
-    mInfo.touchableRegion.clear();
+    windowInfo->touchableRegion.clear();
 
     jobject tokenObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.token);
     if (tokenObj) {
-        mInfo.token = ibinderForJavaObject(env, tokenObj);
+        windowInfo->token = ibinderForJavaObject(env, tokenObj);
         env->DeleteLocalRef(tokenObj);
     } else {
-        mInfo.token.clear();
+        windowInfo->token.clear();
     }
 
-    mInfo.name = getStringField(env, obj, gInputWindowHandleClassInfo.name, "<null>");
+    windowInfo->name = getStringField(env, obj, gInputWindowHandleClassInfo.name, "<null>");
 
-    mInfo.dispatchingTimeout = std::chrono::milliseconds(
+    windowInfo->dispatchingTimeout = std::chrono::milliseconds(
             env->GetLongField(obj, gInputWindowHandleClassInfo.dispatchingTimeoutMillis));
 
     ScopedLocalRef<jobject> frameObj(env,
                                      env->GetObjectField(obj, gInputWindowHandleClassInfo.frame));
-    mInfo.frame = JNICommon::rectFromObj(env, frameObj.get());
+    windowInfo->frame = JNICommon::rectFromObj(env, frameObj.get());
 
-    mInfo.surfaceInset = env->GetIntField(obj,
-            gInputWindowHandleClassInfo.surfaceInset);
-    mInfo.globalScaleFactor = env->GetFloatField(obj,
-            gInputWindowHandleClassInfo.scaleFactor);
+    windowInfo->surfaceInset = env->GetIntField(obj, gInputWindowHandleClassInfo.surfaceInset);
+    windowInfo->globalScaleFactor =
+            env->GetFloatField(obj, gInputWindowHandleClassInfo.scaleFactor);
 
-    jobject regionObj = env->GetObjectField(obj,
-            gInputWindowHandleClassInfo.touchableRegion);
+    jobject regionObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.touchableRegion);
     if (regionObj) {
         for (graphics::RegionIterator it(env, regionObj); !it.isDone(); it.next()) {
             ARect rect = it.getRect();
-            mInfo.addTouchableRegion(Rect(rect.left, rect.top, rect.right, rect.bottom));
+            windowInfo->addTouchableRegion(Rect(rect.left, rect.top, rect.right, rect.bottom));
         }
         env->DeleteLocalRef(regionObj);
     }
@@ -153,49 +138,55 @@
             env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsFlags));
     const auto type = static_cast<WindowInfo::Type>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.layoutParamsType));
-    mInfo.layoutParamsFlags = flags;
-    mInfo.layoutParamsType = type;
+    windowInfo->layoutParamsFlags = flags;
+    windowInfo->layoutParamsType = type;
 
-    mInfo.inputConfig = static_cast<gui::WindowInfo::InputConfig>(
+    windowInfo->inputConfig = static_cast<gui::WindowInfo::InputConfig>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.inputConfig));
 
-    mInfo.touchOcclusionMode = static_cast<TouchOcclusionMode>(
+    windowInfo->touchOcclusionMode = static_cast<TouchOcclusionMode>(
             env->GetIntField(obj, gInputWindowHandleClassInfo.touchOcclusionMode));
-    mInfo.ownerPid = gui::Pid{env->GetIntField(obj, gInputWindowHandleClassInfo.ownerPid)};
-    mInfo.ownerUid = gui::Uid{
+    windowInfo->ownerPid = gui::Pid{env->GetIntField(obj, gInputWindowHandleClassInfo.ownerPid)};
+    windowInfo->ownerUid = gui::Uid{
             static_cast<uid_t>(env->GetIntField(obj, gInputWindowHandleClassInfo.ownerUid))};
-    mInfo.packageName = getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>");
-    mInfo.displayId =
+    windowInfo->packageName =
+            getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>");
+    windowInfo->displayId =
             ui::LogicalDisplayId{env->GetIntField(obj, gInputWindowHandleClassInfo.displayId)};
 
-    jobject inputApplicationHandleObj = env->GetObjectField(obj,
-            gInputWindowHandleClassInfo.inputApplicationHandle);
+    jobject inputApplicationHandleObj =
+            env->GetObjectField(obj, gInputWindowHandleClassInfo.inputApplicationHandle);
     if (inputApplicationHandleObj) {
         std::shared_ptr<InputApplicationHandle> inputApplicationHandle =
                 android_view_InputApplicationHandle_getHandle(env, inputApplicationHandleObj);
         if (inputApplicationHandle != nullptr) {
             inputApplicationHandle->updateInfo();
-            mInfo.applicationInfo = *(inputApplicationHandle->getInfo());
+            windowInfo->applicationInfo = *(inputApplicationHandle->getInfo());
         }
         env->DeleteLocalRef(inputApplicationHandleObj);
     }
 
-    mInfo.replaceTouchableRegionWithCrop = env->GetBooleanField(obj,
-            gInputWindowHandleClassInfo.replaceTouchableRegionWithCrop);
+    windowInfo->replaceTouchableRegionWithCrop =
+            env->GetBooleanField(obj, gInputWindowHandleClassInfo.replaceTouchableRegionWithCrop);
 
-    jobject weakSurfaceCtrl = env->GetObjectField(obj,
-            gInputWindowHandleClassInfo.touchableRegionSurfaceControl.ctrl);
+    jobject weakSurfaceCtrl =
+            env->GetObjectField(obj,
+                                gInputWindowHandleClassInfo.touchableRegionSurfaceControl.ctrl);
     bool touchableRegionCropHandleSet = false;
     if (weakSurfaceCtrl) {
         // Promote java weak reference.
-        jobject strongSurfaceCtrl = env->CallObjectMethod(weakSurfaceCtrl,
-                gInputWindowHandleClassInfo.touchableRegionSurfaceControl.get);
+        jobject strongSurfaceCtrl =
+                env->CallObjectMethod(weakSurfaceCtrl,
+                                      gInputWindowHandleClassInfo.touchableRegionSurfaceControl
+                                              .get);
         if (strongSurfaceCtrl) {
-            jlong mNativeObject = env->GetLongField(strongSurfaceCtrl,
-                    gInputWindowHandleClassInfo.touchableRegionSurfaceControl.mNativeObject);
+            jlong mNativeObject =
+                    env->GetLongField(strongSurfaceCtrl,
+                                      gInputWindowHandleClassInfo.touchableRegionSurfaceControl
+                                              .mNativeObject);
             if (mNativeObject) {
                 auto ctrl = reinterpret_cast<SurfaceControl *>(mNativeObject);
-                mInfo.touchableRegionCropHandle = ctrl->getHandle();
+                windowInfo->touchableRegionCropHandle = ctrl->getHandle();
                 touchableRegionCropHandleSet = true;
             }
             env->DeleteLocalRef(strongSurfaceCtrl);
@@ -203,15 +194,15 @@
         env->DeleteLocalRef(weakSurfaceCtrl);
     }
     if (!touchableRegionCropHandleSet) {
-        mInfo.touchableRegionCropHandle.clear();
+        windowInfo->touchableRegionCropHandle.clear();
     }
 
     jobject windowTokenObj = env->GetObjectField(obj, gInputWindowHandleClassInfo.windowToken);
     if (windowTokenObj) {
-        mInfo.windowToken = ibinderForJavaObject(env, windowTokenObj);
+        windowInfo->windowToken = ibinderForJavaObject(env, windowTokenObj);
         env->DeleteLocalRef(windowTokenObj);
     } else {
-        mInfo.windowToken.clear();
+        windowInfo->windowToken.clear();
     }
 
     ScopedLocalRef<jobject>
@@ -220,41 +211,16 @@
                                                        gInputWindowHandleClassInfo
                                                                .focusTransferTarget));
     if (focusTransferTargetObj.get()) {
-        mInfo.focusTransferTarget = ibinderForJavaObject(env, focusTransferTargetObj.get());
+        windowInfo->focusTransferTarget = ibinderForJavaObject(env, focusTransferTargetObj.get());
     } else {
-        mInfo.focusTransferTarget.clear();
+        windowInfo->focusTransferTarget.clear();
     }
 
-    env->DeleteLocalRef(obj);
-    return true;
-}
-
-
-// --- Global functions ---
-
-sp<NativeInputWindowHandle> android_view_InputWindowHandle_getHandle(
-        JNIEnv* env, jobject inputWindowHandleObj) {
-    if (!inputWindowHandleObj) {
-        return NULL;
-    }
-
-    AutoMutex _l(gHandleMutex);
-
-    jlong ptr = env->GetLongField(inputWindowHandleObj, gInputWindowHandleClassInfo.ptr);
-    NativeInputWindowHandle* handle;
-    if (ptr) {
-        handle = reinterpret_cast<NativeInputWindowHandle*>(ptr);
-    } else {
-        jweak objWeak = env->NewWeakGlobalRef(inputWindowHandleObj);
-        handle = new NativeInputWindowHandle(objWeak);
-        handle->incStrong((void*)android_view_InputWindowHandle_getHandle);
-        env->SetLongField(inputWindowHandleObj, gInputWindowHandleClassInfo.ptr,
-                reinterpret_cast<jlong>(handle));
-    }
     return handle;
 }
 
-jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env, gui::WindowInfo windowInfo) {
+jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env,
+                                                      const gui::WindowInfo& windowInfo) {
     ScopedLocalRef<jobject>
             applicationHandle(env,
                               android_view_InputApplicationHandle_fromInputApplicationInfo(
@@ -337,18 +303,15 @@
 // --- JNI ---
 
 static void android_view_InputWindowHandle_nativeDispose(JNIEnv* env, jobject obj) {
-    AutoMutex _l(gHandleMutex);
-
     jlong ptr = env->GetLongField(obj, gInputWindowHandleClassInfo.ptr);
-    if (ptr) {
-        env->SetLongField(obj, gInputWindowHandleClassInfo.ptr, 0);
-
-        NativeInputWindowHandle* handle = reinterpret_cast<NativeInputWindowHandle*>(ptr);
-        handle->decStrong((void*)android_view_InputWindowHandle_getHandle);
+    if (!ptr) {
+        return;
     }
+    env->SetLongField(obj, gInputWindowHandleClassInfo.ptr, 0);
+    auto handle = reinterpret_cast<gui::WindowInfoHandle*>(ptr);
+    handle->decStrong((void*)android_view_InputWindowHandle_getHandle);
 }
 
-
 static const JNINativeMethod gInputWindowHandleMethods[] = {
     /* name, signature, funcPtr */
     { "nativeDispose", "()V",
diff --git a/core/jni/android_hardware_input_InputWindowHandle.h b/core/jni/android_hardware_input_InputWindowHandle.h
index 408e0f1..aa375e9 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.h
+++ b/core/jni/android_hardware_input_InputWindowHandle.h
@@ -24,24 +24,11 @@
 
 namespace android {
 
-class NativeInputWindowHandle : public gui::WindowInfoHandle {
-public:
-    NativeInputWindowHandle(jweak objWeak);
-    virtual ~NativeInputWindowHandle();
+sp<gui::WindowInfoHandle> android_view_InputWindowHandle_getHandle(JNIEnv* env,
+                                                                   jobject inputWindowHandleObj);
 
-    jobject getInputWindowHandleObjLocalRef(JNIEnv* env);
-
-    virtual bool updateInfo();
-
-private:
-    jweak mObjWeak;
-};
-
-extern sp<NativeInputWindowHandle> android_view_InputWindowHandle_getHandle(
-        JNIEnv* env, jobject inputWindowHandleObj);
-
-extern jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env,
-                                                             gui::WindowInfo windowInfo);
+jobject android_view_InputWindowHandle_fromWindowInfo(JNIEnv* env,
+                                                      const gui::WindowInfo& windowInfo);
 
 } // namespace android
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 56292c3..d3bf36e 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -979,14 +979,16 @@
 
 static void nativeSetInputWindowInfo(JNIEnv* env, jclass clazz, jlong transactionObj,
         jlong nativeObject, jobject inputWindow) {
+    if (!inputWindow) {
+        jniThrowNullPointerException(env, "InputWindowHandle is null");
+        return;
+    }
+
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
 
-    sp<NativeInputWindowHandle> handle = android_view_InputWindowHandle_getHandle(
-            env, inputWindow);
-    handle->updateInfo();
-
+    sp<gui::WindowInfoHandle> info = android_view_InputWindowHandle_getHandle(env, inputWindow);
     auto ctrl = reinterpret_cast<SurfaceControl *>(nativeObject);
-    transaction->setInputWindowInfo(ctrl, *handle->getInfo());
+    transaction->setInputWindowInfo(ctrl, std::move(info));
 }
 
 static void nativeAddWindowInfosReportedListener(JNIEnv* env, jclass clazz, jlong transactionObj,
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 606e829..6af742f 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -104,6 +104,7 @@
         optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_gesture_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     }
     optional Accessibility accessibility = 2;
diff --git a/core/res/Android.bp b/core/res/Android.bp
index 66c2e12..0e4e22b 100644
--- a/core/res/Android.bp
+++ b/core/res/Android.bp
@@ -158,6 +158,7 @@
     flags_packages: [
         "android.app.appfunctions.flags-aconfig",
         "android.app.contextualsearch.flags-aconfig",
+        "android.app.flags-aconfig",
         "android.appwidget.flags-aconfig",
         "android.content.pm.flags-aconfig",
         "android.provider.flags-aconfig",
@@ -171,7 +172,9 @@
         "android.security.flags-aconfig",
         "com.android.hardware.input.input-aconfig",
         "aconfig_trade_in_mode_flags",
+        "art-aconfig-flags",
         "ranging_aconfig_flags",
+        "aconfig_settingslib_flags",
     ],
 }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5913992..d09802a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2632,13 +2632,22 @@
 
     <!-- @SystemApi Allows access to perform vendor effects in the vibrator.
          <p>Protection level: signature
-         @FlaggedApi("android.os.vibrator.vendor_vibration_effects")
+         @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
          @hide
     -->
     <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS"
         android:protectionLevel="signature|privileged"
         android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
 
+    <!-- @SystemApi Allows access to start a vendor vibration session.
+         <p>Protection level: signature
+        @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+        @hide
+    -->
+    <permission android:name="android.permission.START_VIBRATION_SESSIONS"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="android.os.vibrator.vendor_vibration_effects" />
+
     <!-- @SystemApi Allows access to the vibrator state.
          <p>Protection level: signature
          @hide
@@ -4454,6 +4463,18 @@
                 android:description="@string/permdesc_hideOverlayWindows"
                 android:protectionLevel="normal" />
 
+    <!-- Allows an app to enter Picture-in-Picture mode when the user is not explicitly requesting
+        it. This includes using {@link PictureInPictureParams.Builder#setAutoEnterEnabled} as well
+        as lifecycle methods such as {@link Activity#onUserLeaveHint} and {@link Activity#onPause}
+        to enter PiP when the user leaves the app.
+        This permission should only be used for certain PiP
+        <a href="{@docRoot}training/tv/get-started/multitasking#usage-types">usage types</a>.
+        @FlaggedApi(android.app.Flags.FLAG_ENABLE_TV_IMPLICIT_ENTER_PIP_RESTRICTION)
+     -->
+    <permission android:name="android.permission.TV_IMPLICIT_ENTER_PIP"
+        android:protectionLevel="normal"
+        android:featureFlag="android.app.enable_tv_implicit_enter_pip_restriction" />
+
     <!-- ================================== -->
     <!-- Permissions affecting the system wallpaper -->
     <!-- ================================== -->
@@ -4960,6 +4981,27 @@
     <permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS"
                 android:protectionLevel="signature|privileged|role" />
 
+    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST)
+         Allows an application to access the Settings Preference services to read settings exposed
+         by the system Settings app and system apps that contribute settings surfaced by the
+         Settings app.
+         <p>This allows the calling application to read settings values through the host
+         application, agnostic of underlying storage. -->
+    <permission android:name="android.permission.READ_SYSTEM_PREFERENCES"
+        android:protectionLevel="signature|privileged|role"
+        android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
+
+    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST)
+         Allows an application to access the Settings Preference services to write settings
+         values exposed by the system Settings app and system apps that contribute settings surfaced
+         in the Settings app.
+         <p>This allows the calling application to write settings values
+         through the host application, agnostic of underlying storage.
+         <p>Protection Level: signature|privileged|appop - appop to be added in followup -->
+    <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES"
+        android:protectionLevel="signature|privileged"
+        android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
+
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
     <!-- ========================================= -->
@@ -8482,6 +8524,16 @@
                 android:protectionLevel="signature|privileged"
                 android:featureFlag="com.android.tradeinmode.flags.enable_trade_in_mode" />
 
+    <!-- @SystemApi
+        @FlaggedApi(com.android.art.flags.Flags.FLAG_EXECUTABLE_METHOD_FILE_OFFSETS)
+        Ability to read program metadata and attach dynamic instrumentation.
+        <p>Protection level: signature
+        @hide
+    -->
+    <permission android:name="android.permission.DYNAMIC_INSTRUMENTATION"
+                android:protectionLevel="signature"
+                android:featureFlag="com.android.art.flags.executable_method_file_offsets" />
+
     <!--
         @TestApi
         Signature permission reserved for testing. This should never be used to
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 4d73f22..41dec37 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2273,6 +2273,22 @@
         <attr name="enableOnBackInvokedCallback" format="boolean"/>
 
         <attr name="intentMatchingFlags"/>
+
+        <!-- Specifies the set of drawable resources that can be used in place
+             of an existing declared icon or banner for activities that appear
+             in the app launcher. The resource referenced must be an array of
+             drawable resources and can contain at most 500 items.
+             {@link android.content.pm.PackageManager#changeLauncherIconConfig}
+             @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
+        <attr name="alternateLauncherIcons" format="reference" />
+
+        <!-- Specifies the set of string resources that can be used in place
+             of an existing declared label for activities that appear
+             in the app launcher. The resource referenced must be an array of
+             string resources and can contain at most 500 items.
+             {@link android.content.pm.PackageManager#changeLauncherIconConfig}
+             @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
+        <attr name="alternateLauncherLabels" format="reference" />
     </declare-styleable>
 
     <!-- An attribution is a logical part of an app and is identified by a tag.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7402a2f..219cefd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4596,6 +4596,11 @@
          exists on the device, the accessibility shortcut will be disabled by default. -->
     <string name="config_defaultAccessibilityService" translatable="false"></string>
 
+    <!-- The component name, flattened to a string, for the default select to speak service to be
+         enabled by the accessibility keyboard shortcut. If the service with the specified component
+         name is not preinstalled then this shortcut will do nothing. -->
+    <string name="config_defaultSelectToSpeakService" translatable="false"></string>
+
     <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. -->
     <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string>
 
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 70cc5f1..b6436d0 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -127,6 +127,10 @@
     <public name="intentMatchingFlags"/>
     <!-- @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) -->
     <public name="layoutLabel"/>
+    <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
+    <public name="alternateLauncherIcons"/>
+    <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) -->
+    <public name="alternateLauncherLabels"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01b60000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index db81a3b..badb986 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3718,6 +3718,7 @@
   <java-symbol type="string" name="color_correction_feature_name" />
   <java-symbol type="string" name="reduce_bright_colors_feature_name" />
   <java-symbol type="string" name="config_defaultAccessibilityService" />
+  <java-symbol type="string" name="config_defaultSelectToSpeakService" />
   <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" />
   <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" />
   <java-symbol type="array" name="config_trustedAccessibilityServices" />
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index da1fffa..a2598f6 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID;
 import static android.app.PropertyInvalidatedCache.NONCE_UNSET;
 import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH;
 import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
@@ -30,8 +31,9 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import android.app.PropertyInvalidatedCache.Args;
 import android.annotation.SuppressLint;
+import android.app.PropertyInvalidatedCache.Args;
+import android.os.Binder;
 import com.android.internal.os.ApplicationSharedMemory;
 
 import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -58,6 +60,7 @@
  */
 @SmallTest
 public class PropertyInvalidatedCacheTests {
+    @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
 
@@ -455,8 +458,9 @@
     // Test the Args-style constructor.
     @Test
     public void testArgsConstructor() {
-        // Create a cache with a maximum of four entries.
-        TestCache cache = new TestCache(new Args(MODULE_TEST).api("init1").maxEntries(4),
+        // Create a cache with a maximum of four entries and non-isolated UIDs.
+        TestCache cache = new TestCache(new Args(MODULE_TEST)
+                .maxEntries(4).isolateUids(false).api("init1"),
                 new TestQuery());
 
         cache.invalidateCache();
@@ -570,4 +574,73 @@
             // Expected exception.
         }
     }
+
+    // Verify that a cache created with isolatedUids(true) separates out the results.
+    @RequiresFlagsEnabled(FLAG_PIC_ISOLATE_CACHE_BY_UID)
+    @Test
+    public void testIsolatedUids() {
+        TestCache cache = new TestCache(new Args(MODULE_TEST)
+                .maxEntries(4).isolateUids(true).api("testIsolatedUids").testMode(true),
+                new TestQuery());
+        cache.invalidateCache();
+        final int uid1 = 1;
+        final int uid2 = 2;
+
+        long token = Binder.setCallingWorkSourceUid(uid1);
+        try {
+            // Populate the cache for user 1
+            assertEquals("foo5", cache.query(5));
+            assertEquals(1, cache.getRecomputeCount());
+            assertEquals("foo5", cache.query(5));
+            assertEquals(1, cache.getRecomputeCount());
+            assertEquals("foo6", cache.query(6));
+            assertEquals(2, cache.getRecomputeCount());
+
+            // Populate the cache for user 2.  User 1 values are not reused.
+            Binder.setCallingWorkSourceUid(uid2);
+            assertEquals("foo5", cache.query(5));
+            assertEquals(3, cache.getRecomputeCount());
+            assertEquals("foo5", cache.query(5));
+            assertEquals(3, cache.getRecomputeCount());
+
+            // Verify that the cache for user 1 is still populated.
+            Binder.setCallingWorkSourceUid(uid1);
+            assertEquals("foo5", cache.query(5));
+            assertEquals(3, cache.getRecomputeCount());
+
+        } finally {
+            Binder.restoreCallingWorkSource(token);
+        }
+
+        // Repeat the test with a non-isolated cache.
+        cache = new TestCache(new Args(MODULE_TEST)
+                .maxEntries(4).isolateUids(false).api("testIsolatedUids2").testMode(true),
+                new TestQuery());
+        cache.invalidateCache();
+        token = Binder.setCallingWorkSourceUid(uid1);
+        try {
+            // Populate the cache for user 1
+            assertEquals("foo5", cache.query(5));
+            assertEquals(1, cache.getRecomputeCount());
+            assertEquals("foo5", cache.query(5));
+            assertEquals(1, cache.getRecomputeCount());
+            assertEquals("foo6", cache.query(6));
+            assertEquals(2, cache.getRecomputeCount());
+
+            // Populate the cache for user 2.  User 1 values are reused.
+            Binder.setCallingWorkSourceUid(uid2);
+            assertEquals("foo5", cache.query(5));
+            assertEquals(2, cache.getRecomputeCount());
+            assertEquals("foo5", cache.query(5));
+            assertEquals(2, cache.getRecomputeCount());
+
+            // Verify that the cache for user 1 is still populated.
+            Binder.setCallingWorkSourceUid(uid1);
+            assertEquals("foo5", cache.query(5));
+            assertEquals(2, cache.getRecomputeCount());
+
+        } finally {
+            Binder.restoreCallingWorkSource(token);
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt
new file mode 100644
index 0000000..a6de611
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt
@@ -0,0 +1,472 @@
+/*
+ * 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.hardware.display
+
+import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM
+import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
+import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
+import android.view.Display
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class DisplayTopologyTest {
+    private var topology = DisplayTopology()
+
+    @Test
+    fun addOneDisplay() {
+        val displayId = 1
+        val width = 800f
+        val height = 600f
+
+        topology.addDisplay(displayId, width, height)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId)
+
+        val display = topology.root!!
+        assertThat(display.displayId).isEqualTo(displayId)
+        assertThat(display.width).isEqualTo(width)
+        assertThat(display.height).isEqualTo(height)
+        assertThat(display.children).isEmpty()
+    }
+
+    @Test
+    fun addTwoDisplays() {
+        val displayId1 = 1
+        val width1 = 800f
+        val height1 = 600f
+
+        val displayId2 = 2
+        val width2 = 1000f
+        val height2 = 1500f
+
+        topology.addDisplay(displayId1, width1, height1)
+        topology.addDisplay(displayId2, width2, height2)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
+
+        val display1 = topology.root!!
+        assertThat(display1.displayId).isEqualTo(displayId1)
+        assertThat(display1.width).isEqualTo(width1)
+        assertThat(display1.height).isEqualTo(height1)
+        assertThat(display1.children).hasSize(1)
+
+        val display2 = display1.children[0]
+        assertThat(display2.displayId).isEqualTo(displayId2)
+        assertThat(display2.width).isEqualTo(width2)
+        assertThat(display2.height).isEqualTo(height2)
+        assertThat(display2.children).isEmpty()
+        assertThat(display2.position).isEqualTo(POSITION_TOP)
+        assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+    }
+
+    @Test
+    fun addManyDisplays() {
+        val displayId1 = 1
+        val width1 = 800f
+        val height1 = 600f
+
+        val displayId2 = 2
+        val width2 = 1000f
+        val height2 = 1500f
+
+        topology.addDisplay(displayId1, width1, height1)
+        topology.addDisplay(displayId2, width2, height2)
+
+        val noOfDisplays = 30
+        for (i in 3..noOfDisplays) {
+            topology.addDisplay(/* displayId= */ i, width1, height1)
+        }
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
+
+        val display1 = topology.root!!
+        assertThat(display1.displayId).isEqualTo(displayId1)
+        assertThat(display1.width).isEqualTo(width1)
+        assertThat(display1.height).isEqualTo(height1)
+        assertThat(display1.children).hasSize(1)
+
+        val display2 = display1.children[0]
+        assertThat(display2.displayId).isEqualTo(displayId2)
+        assertThat(display2.width).isEqualTo(width2)
+        assertThat(display2.height).isEqualTo(height2)
+        assertThat(display2.children).hasSize(1)
+        assertThat(display2.position).isEqualTo(POSITION_TOP)
+        assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+
+        var display = display2
+        for (i in 3..noOfDisplays) {
+            display = display.children[0]
+            assertThat(display.displayId).isEqualTo(i)
+            assertThat(display.width).isEqualTo(width1)
+            assertThat(display.height).isEqualTo(height1)
+            // The last display should have no children
+            assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
+            assertThat(display.position).isEqualTo(POSITION_RIGHT)
+            assertThat(display.offset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun removeDisplays() {
+        val displayId1 = 1
+        val width1 = 800f
+        val height1 = 600f
+
+        val displayId2 = 2
+        val width2 = 1000f
+        val height2 = 1500f
+
+        topology.addDisplay(displayId1, width1, height1)
+        topology.addDisplay(displayId2, width2, height2)
+
+        val noOfDisplays = 30
+        for (i in 3..noOfDisplays) {
+            topology.addDisplay(/* displayId= */ i, width1, height1)
+        }
+
+        var removedDisplays = arrayOf(20)
+        topology.removeDisplay(20)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
+
+        var display1 = topology.root!!
+        assertThat(display1.displayId).isEqualTo(displayId1)
+        assertThat(display1.width).isEqualTo(width1)
+        assertThat(display1.height).isEqualTo(height1)
+        assertThat(display1.children).hasSize(1)
+
+        var display2 = display1.children[0]
+        assertThat(display2.displayId).isEqualTo(displayId2)
+        assertThat(display2.width).isEqualTo(width2)
+        assertThat(display2.height).isEqualTo(height2)
+        assertThat(display2.children).hasSize(1)
+        assertThat(display2.position).isEqualTo(POSITION_TOP)
+        assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+
+        var display = display2
+        for (i in 3..noOfDisplays) {
+            if (i in removedDisplays) {
+                continue
+            }
+            display = display.children[0]
+            assertThat(display.displayId).isEqualTo(i)
+            assertThat(display.width).isEqualTo(width1)
+            assertThat(display.height).isEqualTo(height1)
+            // The last display should have no children
+            assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
+            assertThat(display.position).isEqualTo(POSITION_RIGHT)
+            assertThat(display.offset).isEqualTo(0)
+        }
+
+        topology.removeDisplay(22)
+        removedDisplays += 22
+        topology.removeDisplay(23)
+        removedDisplays += 23
+        topology.removeDisplay(25)
+        removedDisplays += 25
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
+
+        display1 = topology.root!!
+        assertThat(display1.displayId).isEqualTo(displayId1)
+        assertThat(display1.width).isEqualTo(width1)
+        assertThat(display1.height).isEqualTo(height1)
+        assertThat(display1.children).hasSize(1)
+
+        display2 = display1.children[0]
+        assertThat(display2.displayId).isEqualTo(displayId2)
+        assertThat(display2.width).isEqualTo(width2)
+        assertThat(display2.height).isEqualTo(height2)
+        assertThat(display2.children).hasSize(1)
+        assertThat(display2.position).isEqualTo(POSITION_TOP)
+        assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+
+        display = display2
+        for (i in 3..noOfDisplays) {
+            if (i in removedDisplays) {
+                continue
+            }
+            display = display.children[0]
+            assertThat(display.displayId).isEqualTo(i)
+            assertThat(display.width).isEqualTo(width1)
+            assertThat(display.height).isEqualTo(height1)
+            // The last display should have no children
+            assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
+            assertThat(display.position).isEqualTo(POSITION_RIGHT)
+            assertThat(display.offset).isEqualTo(0)
+        }
+    }
+
+    @Test
+    fun removeAllDisplays() {
+        val displayId = 1
+        val width = 800f
+        val height = 600f
+
+        topology.addDisplay(displayId, width, height)
+        topology.removeDisplay(displayId)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(Display.INVALID_DISPLAY)
+        assertThat(topology.root).isNull()
+    }
+
+    @Test
+    fun removeDisplayThatDoesNotExist() {
+        val displayId = 1
+        val width = 800f
+        val height = 600f
+
+        topology.addDisplay(displayId, width, height)
+        topology.removeDisplay(3)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId)
+
+        val display = topology.root!!
+        assertThat(display.displayId).isEqualTo(displayId)
+        assertThat(display.width).isEqualTo(width)
+        assertThat(display.height).isEqualTo(height)
+        assertThat(display.children).isEmpty()
+    }
+
+    @Test
+    fun removePrimaryDisplay() {
+        val displayId1 = 1
+        val displayId2 = 2
+        val width = 800f
+        val height = 600f
+
+        topology = DisplayTopology(/* root= */ null, displayId2)
+        topology.addDisplay(displayId1, width, height)
+        topology.addDisplay(displayId2, width, height)
+        topology.removeDisplay(displayId2)
+
+        assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
+        val display = topology.root!!
+        assertThat(display.displayId).isEqualTo(displayId1)
+        assertThat(display.width).isEqualTo(width)
+        assertThat(display.height).isEqualTo(height)
+        assertThat(display.children).isEmpty()
+    }
+
+    @Test
+    fun normalization_noOverlaps_leavesTopologyUnchanged() {
+        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
+            /* height= */ 600f, /* position= */ 0, /* offset= */ 0f)
+
+        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f)
+        display1.addChild(display2)
+
+        val primaryDisplayId = 3
+        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f)
+        display1.addChild(display3)
+
+        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
+            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
+        display2.addChild(display4)
+
+        topology = DisplayTopology(display1, primaryDisplayId)
+        topology.normalize()
+
+        assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
+
+        val actualDisplay1 = topology.root!!
+        assertThat(actualDisplay1.displayId).isEqualTo(1)
+        assertThat(actualDisplay1.width).isEqualTo(200f)
+        assertThat(actualDisplay1.height).isEqualTo(600f)
+        assertThat(actualDisplay1.children).hasSize(2)
+
+        val actualDisplay2 = actualDisplay1.children[0]
+        assertThat(actualDisplay2.displayId).isEqualTo(2)
+        assertThat(actualDisplay2.width).isEqualTo(600f)
+        assertThat(actualDisplay2.height).isEqualTo(200f)
+        assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay2.offset).isEqualTo(0f)
+        assertThat(actualDisplay2.children).hasSize(1)
+
+        val actualDisplay3 = actualDisplay1.children[1]
+        assertThat(actualDisplay3.displayId).isEqualTo(3)
+        assertThat(actualDisplay3.width).isEqualTo(600f)
+        assertThat(actualDisplay3.height).isEqualTo(200f)
+        assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay3.offset).isEqualTo(400f)
+        assertThat(actualDisplay3.children).isEmpty()
+
+        val actualDisplay4 = actualDisplay2.children[0]
+        assertThat(actualDisplay4.displayId).isEqualTo(4)
+        assertThat(actualDisplay4.width).isEqualTo(200f)
+        assertThat(actualDisplay4.height).isEqualTo(600f)
+        assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay4.offset).isEqualTo(0f)
+        assertThat(actualDisplay4.children).isEmpty()
+    }
+
+    @Test
+    fun normalization_moveDisplayWithoutReparenting() {
+        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
+            /* height= */ 600f, /* position= */ 0, /* offset= */ 0f)
+
+        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f,
+            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
+        display1.addChild(display2)
+
+        val primaryDisplayId = 3
+        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f)
+        display1.addChild(display3)
+
+        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
+            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
+        display2.addChild(display4)
+
+        topology = DisplayTopology(display1, primaryDisplayId)
+        // Display 3 becomes a child of display 2. Display 4 gets moved without changing its parent.
+        topology.normalize()
+
+        assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
+
+        val actualDisplay1 = topology.root!!
+        assertThat(actualDisplay1.displayId).isEqualTo(1)
+        assertThat(actualDisplay1.width).isEqualTo(200f)
+        assertThat(actualDisplay1.height).isEqualTo(600f)
+        assertThat(actualDisplay1.children).hasSize(1)
+
+        val actualDisplay2 = actualDisplay1.children[0]
+        assertThat(actualDisplay2.displayId).isEqualTo(2)
+        assertThat(actualDisplay2.width).isEqualTo(200f)
+        assertThat(actualDisplay2.height).isEqualTo(600f)
+        assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay2.offset).isEqualTo(0f)
+        assertThat(actualDisplay2.children).hasSize(2)
+
+        val actualDisplay3 = actualDisplay2.children[1]
+        assertThat(actualDisplay3.displayId).isEqualTo(3)
+        assertThat(actualDisplay3.width).isEqualTo(600f)
+        assertThat(actualDisplay3.height).isEqualTo(200f)
+        assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay3.offset).isEqualTo(10f)
+        assertThat(actualDisplay3.children).isEmpty()
+
+        val actualDisplay4 = actualDisplay2.children[0]
+        assertThat(actualDisplay4.displayId).isEqualTo(4)
+        assertThat(actualDisplay4.width).isEqualTo(200f)
+        assertThat(actualDisplay4.height).isEqualTo(600f)
+        assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay4.offset).isEqualTo(210f)
+        assertThat(actualDisplay4.children).isEmpty()
+    }
+
+    @Test
+    fun normalization_moveDisplayWithoutReparenting_offsetOutOfBounds() {
+        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
+            /* height= */ 50f, /* position= */ 0, /* offset= */ 0f)
+
+        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f)
+        display1.addChild(display2)
+
+        val primaryDisplayId = 3
+        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f)
+        display1.addChild(display3)
+
+        topology = DisplayTopology(display1, primaryDisplayId)
+        // Display 3 gets moved and its left side is still on the same line as the right side
+        // of Display 1, but it no longer touches it (the offset is out of bounds), so Display 2
+        // becomes its new parent.
+        topology.normalize()
+
+        assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
+
+        val actualDisplay1 = topology.root!!
+        assertThat(actualDisplay1.displayId).isEqualTo(1)
+        assertThat(actualDisplay1.width).isEqualTo(200f)
+        assertThat(actualDisplay1.height).isEqualTo(50f)
+        assertThat(actualDisplay1.children).hasSize(1)
+
+        val actualDisplay2 = actualDisplay1.children[0]
+        assertThat(actualDisplay2.displayId).isEqualTo(2)
+        assertThat(actualDisplay2.width).isEqualTo(600f)
+        assertThat(actualDisplay2.height).isEqualTo(200f)
+        assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay2.offset).isEqualTo(0f)
+        assertThat(actualDisplay2.children).hasSize(1)
+
+        val actualDisplay3 = actualDisplay2.children[0]
+        assertThat(actualDisplay3.displayId).isEqualTo(3)
+        assertThat(actualDisplay3.width).isEqualTo(600f)
+        assertThat(actualDisplay3.height).isEqualTo(200f)
+        assertThat(actualDisplay3.position).isEqualTo(POSITION_BOTTOM)
+        assertThat(actualDisplay3.offset).isEqualTo(0f)
+        assertThat(actualDisplay3.children).isEmpty()
+    }
+
+    @Test
+    fun normalization_moveAndReparentDisplay() {
+        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
+            /* height= */ 600f, /* position= */ 0, /* offset= */ 0f)
+
+        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f,
+            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
+        display1.addChild(display2)
+
+        val primaryDisplayId = 3
+        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
+            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f)
+        display1.addChild(display3)
+
+        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
+            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
+        display2.addChild(display4)
+
+        topology = DisplayTopology(display1, primaryDisplayId)
+        topology.normalize()
+
+        assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
+
+        val actualDisplay1 = topology.root!!
+        assertThat(actualDisplay1.displayId).isEqualTo(1)
+        assertThat(actualDisplay1.width).isEqualTo(200f)
+        assertThat(actualDisplay1.height).isEqualTo(600f)
+        assertThat(actualDisplay1.children).hasSize(1)
+
+        val actualDisplay2 = actualDisplay1.children[0]
+        assertThat(actualDisplay2.displayId).isEqualTo(2)
+        assertThat(actualDisplay2.width).isEqualTo(200f)
+        assertThat(actualDisplay2.height).isEqualTo(600f)
+        assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay2.offset).isEqualTo(0f)
+        assertThat(actualDisplay2.children).hasSize(1)
+
+        val actualDisplay3 = actualDisplay2.children[0]
+        assertThat(actualDisplay3.displayId).isEqualTo(3)
+        assertThat(actualDisplay3.width).isEqualTo(600f)
+        assertThat(actualDisplay3.height).isEqualTo(200f)
+        assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay3.offset).isEqualTo(400f)
+        assertThat(actualDisplay3.children).hasSize(1)
+
+        val actualDisplay4 = actualDisplay3.children[0]
+        assertThat(actualDisplay4.displayId).isEqualTo(4)
+        assertThat(actualDisplay4.width).isEqualTo(200f)
+        assertThat(actualDisplay4.height).isEqualTo(600f)
+        assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
+        assertThat(actualDisplay4.offset).isEqualTo(-400f)
+        assertThat(actualDisplay4.children).isEmpty()
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/DisplayInfoTest.java b/core/tests/coretests/src/android/view/DisplayInfoTest.java
index 4c5b7e5..8932cf1 100644
--- a/core/tests/coretests/src/android/view/DisplayInfoTest.java
+++ b/core/tests/coretests/src/android/view/DisplayInfoTest.java
@@ -78,6 +78,23 @@
     }
 
     @Test
+    public void testRefreshRateOverride_keepsDisplyInfosEqualWhenOverrideIsSame() {
+        Display.Mode mode = new Display.Mode(
+                /*modeId=*/1, /*width=*/1000, /*height=*/1000, /*refreshRate=*/120);
+        DisplayInfo displayInfo1 = new DisplayInfo();
+        setSupportedMode(displayInfo1, mode);
+        displayInfo1.renderFrameRate = 60;
+        displayInfo1.refreshRateOverride = 30;
+
+        DisplayInfo displayInfo2 = new DisplayInfo();
+        setSupportedMode(displayInfo2, mode);
+        displayInfo2.renderFrameRate = 30;
+        displayInfo2.refreshRateOverride = 30;
+
+        assertTrue(displayInfo1.equals(displayInfo2));
+    }
+
+    @Test
     public void testRefreshRateOverride_makeDisplayInfosDifferent() {
         Display.Mode mode = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, /*refreshRate=*/120);
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
index 8bebc62..1a9af6b 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -123,6 +124,14 @@
     }
 
     @Test
+    public void getShortcutTargets_keyGestureShortcutNoService_emptyResult() {
+        assertThat(
+                ShortcutUtils.getShortcutTargetsFromSettings(
+                        mContext, KEY_GESTURE, mDefaultUserId)
+        ).isEmpty();
+    }
+
+    @Test
     public void getShortcutTargets_softwareShortcut1Service_return1Service() {
         setupShortcutTargets(ONE_COMPONENT, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
         setupShortcutTargets(TWO_COMPONENTS, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java
index 6210a00..09bfadb 100644
--- a/core/tests/vibrator/src/android/os/VibratorTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorTest.java
@@ -110,8 +110,9 @@
 
     @Test
     public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() {
+        int[] vibratorIds = new int[0];
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]);
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
 
         Vibrator.OnVibratorStateChangedListener mockListener =
                 mock(Vibrator.OnVibratorStateChangedListener.class);
@@ -119,7 +120,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
 
         // Never tries to register a listener to an individual vibrator.
         assertFalse(multiVibratorListener.hasRegisteredListeners());
@@ -128,8 +129,9 @@
 
     @Test
     public void onVibratorStateChanged_singleVibrator_forwardsAllCallbacks() {
+        int[] vibratorIds = new int[] { 1 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -138,7 +140,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
@@ -156,8 +158,9 @@
 
     @Test
     public void onVibratorStateChanged_multipleVibrators_triggersOnlyWhenAllVibratorsInitialized() {
+        int[] vibratorIds = new int[] { 1, 2 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -166,7 +169,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false);
@@ -181,8 +184,9 @@
 
     @Test
     public void onVibratorStateChanged_multipleVibrators_stateChangeIsDeduped() {
+        int[] vibratorIds = new int[] { 1, 2 };
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
-        when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 });
+        when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds);
         when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance());
 
         Vibrator.OnVibratorStateChangedListener mockListener =
@@ -191,7 +195,7 @@
                 new SystemVibrator.MultiVibratorStateListener(
                         mTestLooper.getNewExecutor(), mockListener);
 
-        multiVibratorListener.register(mockVibratorManager);
+        multiVibratorListener.register(mockVibratorManager, vibratorIds);
         assertTrue(multiVibratorListener.hasRegisteredListeners());
 
         multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // none
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 7b96699..857df10 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -213,6 +213,8 @@
     <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="gpu_service" />
     <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="keystore" />
 
+    <assign-permission name="android.permission.DYNAMIC_INSTRUMENTATION" uid="uprobestats" />
+
     <split-permission name="android.permission.ACCESS_FINE_LOCATION">
         <new-permission name="android.permission.ACCESS_COARSE_LOCATION" />
     </split-permission>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 56e55df..541ca60 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -402,6 +402,9 @@
         <permission name="android.permission.SHOW_CUSTOMIZED_RESOLVER"/>
         <!-- Permission required for access VIBRATOR_STATE. -->
         <permission name="android.permission.ACCESS_VIBRATOR_STATE"/>
+        <!-- Permission required for vendor vibration effects and sessions. -->
+        <permission name="android.permission.VIBRATE_VENDOR_EFFECTS"/>
+        <permission name="android.permission.START_VIBRATION_SESSIONS"/>
         <!-- Permission required for UsageStatsTest CTS test. -->
         <permission name="android.permission.MANAGE_NOTIFICATIONS"/>
         <!-- Permission required for CompanionDeviceManager CTS test. -->
@@ -591,6 +594,9 @@
         <!-- Permission required for CTS test - AdvancedProtectionManagerTest -->
         <permission name="android.permission.SET_ADVANCED_PROTECTION_MODE" />
         <permission name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" />
+        <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+        <permission name="android.permission.READ_SYSTEM_PREFERENCES" />
+        <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index f8d3bff..2b0802b 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -171,7 +171,7 @@
 # key 149 "KEY_PROG2"
 key 150   EXPLORER
 # key 151 "KEY_MSDOS"
-key 152   POWER
+key 152   LOCK
 # key 153 "KEY_DIRECTION"
 # key 154 "KEY_CYCLEWINDOWS"
 key 155   ENVELOPE
@@ -200,20 +200,20 @@
 key 178   PAGE_DOWN
 key 179   NUMPAD_LEFT_PAREN
 key 180   NUMPAD_RIGHT_PAREN
-# key 181 "KEY_NEW"
+key 181   NEW
 # key 182 "KEY_REDO"
-# key 183   F13
-# key 184   F14
-# key 185   F15
-# key 186   F16
-# key 187   F17
-# key 188   F18
-# key 189   F19
-# key 190   F20
-# key 191   F21
-# key 192   F22
-# key 193   F23
-# key 194   F24
+key 183   F13
+key 184   F14
+key 185   F15
+key 186   F16
+key 187   F17
+key 188   F18
+key 189   F19
+key 190   F20
+key 191   F21
+key 192   F22
+key 193   F23
+key 194   F24
 # key 195 (undefined)
 # key 196 (undefined)
 # key 197 (undefined)
@@ -225,11 +225,11 @@
 # key 203 "KEY_PROG4"
 key 204 NOTIFICATION
 # key 205 "KEY_SUSPEND"
-# key 206 "KEY_CLOSE"
+key 206   CLOSE
 key 207   MEDIA_PLAY
 key 208   MEDIA_FAST_FORWARD
 # key 209 "KEY_BASSBOOST"
-# key 210 "KEY_PRINT"
+key 210   PRINT
 # key 211 "KEY_HP"
 key 212   CAMERA
 key 213   MUSIC
@@ -328,7 +328,7 @@
 # key 369 "KEY_TITLE"
 key 370   CAPTIONS
 # key 371 "KEY_ANGLE"
-# key 372 "KEY_ZOOM"
+key 372   FULLSCREEN
 # key 373 "KEY_MODE"
 # key 374 "KEY_KEYBOARD"
 # key 375 "KEY_SCREEN"
@@ -425,12 +425,15 @@
 # Linux KEY_ASSISTANT
 key 583   ASSIST
 key 585   EMOJI_PICKER
+key 586   DICTATE
 key 656   MACRO_1
 key 657   MACRO_2
 key 658   MACRO_3
 key 659   MACRO_4
 
 # Keys defined by HID usages
+key usage 0x010082 LOCK                      FALLBACK_USAGE_MAPPING
+key usage 0x01009B DO_NOT_DISTURB            FALLBACK_USAGE_MAPPING
 key usage 0x0c0065 SCREENSHOT                FALLBACK_USAGE_MAPPING
 key usage 0x0c0067 WINDOW                    FALLBACK_USAGE_MAPPING
 key usage 0x0c006F BRIGHTNESS_UP             FALLBACK_USAGE_MAPPING
@@ -438,12 +441,17 @@
 key usage 0x0c0079 KEYBOARD_BACKLIGHT_UP     FALLBACK_USAGE_MAPPING
 key usage 0x0c007A KEYBOARD_BACKLIGHT_DOWN   FALLBACK_USAGE_MAPPING
 key usage 0x0c007C KEYBOARD_BACKLIGHT_TOGGLE FALLBACK_USAGE_MAPPING
+key usage 0x0c00D8 DICTATE                   FALLBACK_USAGE_MAPPING
 key usage 0x0c00D9 EMOJI_PICKER              FALLBACK_USAGE_MAPPING
 key usage 0x0c0173 MEDIA_AUDIO_TRACK         FALLBACK_USAGE_MAPPING
 key usage 0x0c019C PROFILE_SWITCH            FALLBACK_USAGE_MAPPING
 key usage 0x0c019F SETTINGS                  FALLBACK_USAGE_MAPPING
 key usage 0x0c01A2 ALL_APPS                  FALLBACK_USAGE_MAPPING
+key usage 0x0c0201 NEW                       FALLBACK_USAGE_MAPPING
+key usage 0x0c0203 CLOSE                     FALLBACK_USAGE_MAPPING
+key usage 0x0c0208 PRINT                     FALLBACK_USAGE_MAPPING
 key usage 0x0c0227 REFRESH                   FALLBACK_USAGE_MAPPING
+key usage 0x0c0232 FULLSCREEN                FALLBACK_USAGE_MAPPING
 key usage 0x0c029D LANGUAGE_SWITCH           FALLBACK_USAGE_MAPPING
 key usage 0x0c029F RECENT_APPS               FALLBACK_USAGE_MAPPING
 key usage 0x0c02A2 ALL_APPS                  FALLBACK_USAGE_MAPPING
diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java
index 4c47de0..d55a71e 100644
--- a/graphics/java/android/graphics/ColorSpace.java
+++ b/graphics/java/android/graphics/ColorSpace.java
@@ -1761,7 +1761,7 @@
 
         if (Flags.displayBt2020Colorspace()) {
             sNamedColorSpaceMap.put(Named.DISPLAY_BT2020.ordinal(), new ColorSpace.Rgb(
-                    "BT 2020",
+                    "Display BT. 2020",
                     BT2020_PRIMARIES,
                     ILLUMINANT_D65,
                     null,
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 8bb32568..56bb0f0 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2119,7 +2119,7 @@
      * @see FontVariationAxis
      */
     public boolean setFontVariationSettings(String fontVariationSettings) {
-        final boolean useFontVariationStore = Flags.typefaceRedesign()
+        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
                 && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
         if (useFontVariationStore) {
             FontVariationAxis[] axes =
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index ed17fde..43216ba 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -133,7 +133,7 @@
     @NonNull
     public Font getFont(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
-        if (Flags.typefaceRedesign()) {
+        if (Flags.typefaceRedesignReadonly()) {
             return mFonts.get(nGetFontId(mLayoutPtr, index));
         }
         return mFonts.get(index);
@@ -252,7 +252,7 @@
         mXOffset = xOffset;
         mYOffset = yOffset;
 
-        if (Flags.typefaceRedesign()) {
+        if (Flags.typefaceRedesignReadonly()) {
             int fontCount = nGetFontCount(layoutPtr);
             mFonts = new ArrayList<>(fontCount);
             for (int i = 0; i < fontCount; ++i) {
diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java
index 197aaba..e6091c1 100644
--- a/keystore/java/android/security/keystore/KeyStoreManager.java
+++ b/keystore/java/android/security/keystore/KeyStoreManager.java
@@ -49,7 +49,7 @@
  */
 @FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
 @SystemService(Context.KEYSTORE_SERVICE)
-public class KeyStoreManager {
+public final class KeyStoreManager {
     private static final String TAG = "KeyStoreManager";
 
     private static final Object sInstanceLock = new Object();
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
index 501bedd..c2755ef 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -19,6 +19,7 @@
     android:layout_height="wrap_content"
     android:layout_width="wrap_content"
     android:orientation="vertical"
+    android:clipChildren="false"
     android:id="@+id/bubble_expanded_view">
 
     <com.android.wm.shell.bubbles.bar.BubbleBarHandleView
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
index f1ecde4..7aca921 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
@@ -14,20 +14,18 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<com.android.wm.shell.bubbles.bar.BubbleBarMenuView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<com.android.wm.shell.bubbles.bar.BubbleBarMenuView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center_horizontal"
+    android:clipToPadding="false"
     android:minWidth="@dimen/bubble_bar_manage_menu_min_width"
     android:orientation="vertical"
-    android:elevation="@dimen/bubble_manage_menu_elevation"
-    android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top"
-    android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding"
-    android:paddingBottom="@dimen/bubble_bar_manage_menu_padding"
-    android:clipToPadding="false">
+    android:visibility="invisible"
+    tools:visibility="visible">
 
     <LinearLayout
         android:id="@+id/bubble_bar_manage_menu_bubble_section"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index f90e165..a18a251 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -168,7 +168,7 @@
     </LinearLayout>
 
     <LinearLayout
-        android:id="@+id/open_in_browser_pill"
+        android:id="@+id/open_in_app_or_browser_pill"
         android:layout_width="match_parent"
         android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
@@ -178,7 +178,7 @@
         android:background="@drawable/desktop_mode_decor_handle_menu_background">
 
         <Button
-            android:id="@+id/open_in_browser_button"
+            android:id="@+id/open_in_app_or_browser_button"
             android:layout_weight="1"
             android:contentDescription="@string/open_in_browser_text"
             android:text="@string/open_in_browser_text"
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 8f1ef6c..012579a 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -301,6 +301,8 @@
     <string name="screenshot_text">Screenshot</string>
     <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] -->
     <string name="open_in_browser_text">Open in browser</string>
+    <!-- Accessibility text for the handle menu open in app button [CHAR LIMIT=NONE] -->
+    <string name="open_in_app_text">Open in App</string>
     <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] -->
     <string name="new_window_text">New Window</string>
     <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 65132fe..7243ea3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -20,7 +20,9 @@
 
 import android.content.Context
 import android.content.Intent
+import android.content.Intent.ACTION_VIEW
 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainVerificationManager
 import android.content.pm.verify.domain.DomainVerificationUserState
@@ -31,7 +33,7 @@
 private const val TAG = "AppToWebUtils"
 
 private val GenericBrowserIntent = Intent()
-    .setAction(Intent.ACTION_VIEW)
+    .setAction(ACTION_VIEW)
     .addCategory(Intent.CATEGORY_BROWSABLE)
     .setData(Uri.parse("http:"))
 
@@ -67,6 +69,20 @@
 }
 
 /**
+ * Returns intent if there is a non-browser application available to handle the uri. Otherwise,
+ * returns null.
+ */
+fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? {
+    val intent = Intent(ACTION_VIEW, uri).apply {
+        flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER
+    }
+    // If there is no application available to handle intent, return null
+    val component = intent.resolveActivity(packageManager) ?: return null
+    intent.setComponent(component)
+    return intent
+}
+
+/**
  * Returns the [DomainVerificationUserState] of the user associated with the given
  * [DomainVerificationManager] and the given package.
  */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index c92a278..ce7a977 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -1122,7 +1122,7 @@
         final BackMotionEvent backFinish = mCurrentTracker
                 .createProgressEvent();
         dispatchOnBackProgressed(mActiveCallback, backFinish);
-        if (!mBackGestureStarted) {
+        if (mCurrentTracker.isFinished()) {
             // if the down -> up gesture happened before animation
             // start, we have to trigger the uninterruptible transition
             // to finish the back animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 2a50e4d0..272dfec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -222,7 +222,7 @@
 
             mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate());
         }
-        mMenuViewController = new BubbleBarMenuViewController(mContext, this);
+        mMenuViewController = new BubbleBarMenuViewController(mContext, mHandleView, this);
         mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
             @Override
             public void onMenuVisibilityChanged(boolean visible) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
index e781c07..712e41b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
@@ -17,17 +17,18 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.graphics.Outline;
-import android.graphics.Path;
-import android.graphics.RectF;
+import android.graphics.Canvas;
+import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.view.View;
-import android.view.ViewOutlineProvider;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.animation.IntProperty;
 import androidx.core.content.ContextCompat;
 
 import com.android.wm.shell.R;
@@ -37,14 +38,33 @@
  */
 public class BubbleBarHandleView extends View {
     private static final long COLOR_CHANGE_DURATION = 120;
-    // Path used to draw the dots
-    private final Path mPath = new Path();
 
+    /** Custom property to set handle color. */
+    private static final IntProperty<BubbleBarHandleView> HANDLE_COLOR = new IntProperty<>(
+            "handleColor") {
+        @Override
+        public void setValue(BubbleBarHandleView bubbleBarHandleView, int color) {
+            bubbleBarHandleView.setHandleColor(color);
+        }
+
+        @Override
+        public Integer get(BubbleBarHandleView bubbleBarHandleView) {
+            return bubbleBarHandleView.getHandleColor();
+        }
+    };
+
+    @VisibleForTesting
+    final Paint mHandlePaint = new Paint();
     private final @ColorInt int mHandleLightColor;
     private final @ColorInt int mHandleDarkColor;
-    private @ColorInt int mCurrentColor;
+    private final ArgbEvaluator mArgbEvaluator = ArgbEvaluator.getInstance();
+    private final float mHandleHeight;
+    private final float mHandleWidth;
+    private float mCurrentHandleHeight;
+    private float mCurrentHandleWidth;
     @Nullable
     private ObjectAnimator mColorChangeAnim;
+    private @ColorInt int mRegionSamplerColor;
 
     public BubbleBarHandleView(Context context) {
         this(context, null /* attrs */);
@@ -61,30 +81,52 @@
     public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        final int handleHeight = getResources().getDimensionPixelSize(
+        mHandlePaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+        mHandlePaint.setStyle(Paint.Style.FILL);
+        mHandlePaint.setColor(0);
+        mHandleHeight = getResources().getDimensionPixelSize(
                 R.dimen.bubble_bar_expanded_view_handle_height);
+        mHandleWidth = getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_caption_width);
         mHandleLightColor = ContextCompat.getColor(getContext(),
                 R.color.bubble_bar_expanded_view_handle_light);
         mHandleDarkColor = ContextCompat.getColor(getContext(),
                 R.color.bubble_bar_expanded_view_handle_dark);
-
-        setClipToOutline(true);
-        setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                final int handleCenterY = view.getHeight() / 2;
-                final int handleTop = handleCenterY - handleHeight / 2;
-                final int handleBottom = handleTop + handleHeight;
-                final int radius = handleHeight / 2;
-                RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom);
-                mPath.reset();
-                mPath.addRoundRect(handle, radius, radius, Path.Direction.CW);
-                outline.setPath(mPath);
-            }
-        });
+        mCurrentHandleHeight = mHandleHeight;
+        mCurrentHandleWidth = mHandleWidth;
         setContentDescription(getResources().getString(R.string.handle_text));
     }
 
+    private void setHandleColor(int color) {
+        mHandlePaint.setColor(color);
+        invalidate();
+    }
+
+    private int getHandleColor() {
+        return mHandlePaint.getColor();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        float handleLeft = (getWidth() - mCurrentHandleWidth) / 2;
+        float handleRight = handleLeft + mCurrentHandleWidth;
+        float handleCenterY = (float) getHeight() / 2;
+        float handleTop = (int) (handleCenterY - mCurrentHandleHeight / 2);
+        float handleBottom = handleTop + mCurrentHandleHeight;
+        float cornerRadius = mCurrentHandleHeight / 2;
+        canvas.drawRoundRect(handleLeft, handleTop, handleRight, handleBottom, cornerRadius,
+                cornerRadius, mHandlePaint);
+    }
+
+    /** Sets handle width, height and color. Does not change the layout properties */
+    private void setHandleProperties(float width, float height, int color) {
+        mCurrentHandleHeight = height;
+        mCurrentHandleWidth = width;
+        mHandlePaint.setColor(color);
+        invalidate();
+    }
+
     /**
      * Updates the handle color.
      *
@@ -94,15 +136,15 @@
      */
     public void updateHandleColor(boolean isRegionDark, boolean animated) {
         int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor;
-        if (newColor == mCurrentColor) {
+        if (newColor == mRegionSamplerColor) {
             return;
         }
+        mRegionSamplerColor = newColor;
         if (mColorChangeAnim != null) {
             mColorChangeAnim.cancel();
         }
-        mCurrentColor = newColor;
         if (animated) {
-            mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor);
+            mColorChangeAnim = ObjectAnimator.ofArgb(this, HANDLE_COLOR, newColor);
             mColorChangeAnim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
@@ -112,7 +154,39 @@
             mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION);
             mColorChangeAnim.start();
         } else {
-            setBackgroundColor(newColor);
+            setHandleColor(newColor);
         }
     }
+
+    /** Returns handle padding top. */
+    public int getHandlePaddingTop() {
+        return (getHeight() - getResources().getDimensionPixelSize(
+                R.dimen.bubble_bar_expanded_view_handle_height)) / 2;
+    }
+
+    /** Animates handle for the bubble menu. */
+    public void animateHandleForMenu(float progress, float widthDelta, float heightDelta,
+            int menuColor) {
+        float currentWidth = mHandleWidth + widthDelta * progress;
+        float currentHeight = mHandleHeight + heightDelta * progress;
+        int color = (int) mArgbEvaluator.evaluate(progress, mRegionSamplerColor, menuColor);
+        setHandleProperties(currentWidth, currentHeight, color);
+        setTranslationY(heightDelta * progress / 2);
+    }
+
+    /** Restores all the properties that were animated to the default values. */
+    public void restoreAnimationDefaults() {
+        setHandleProperties(mHandleWidth, mHandleHeight, mRegionSamplerColor);
+        setTranslationY(0);
+    }
+
+    /** Returns the handle height. */
+    public int getHandleHeight() {
+        return (int) mHandleHeight;
+    }
+
+    /** Returns the handle width. */
+    public int getHandleWidth() {
+        return (int) mHandleWidth;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 999ce17..1f77abe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -19,6 +19,7 @@
 import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN;
 import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT;
 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE;
+import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -66,8 +67,6 @@
 
     private static final String TAG = BubbleBarLayerView.class.getSimpleName();
 
-    private static final float SCRIM_ALPHA = 0.2f;
-
     private final BubbleController mBubbleController;
     private final BubbleData mBubbleData;
     private final BubblePositioner mPositioner;
@@ -386,7 +385,7 @@
         if (show) {
             mScrimView.animate()
                     .setInterpolator(ALPHA_IN)
-                    .alpha(SCRIM_ALPHA)
+                    .alpha(BUBBLE_EXPANDED_SCRIM_ALPHA)
                     .start();
         } else {
             mScrimView.animate()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
index 0ee20ef..99e2009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -47,6 +47,10 @@
     private ImageView mBubbleIconView;
     private ImageView mBubbleDismissIconView;
     private TextView mBubbleTitleView;
+    // The animation has three stages. Each stage transition lasts until the animation ends. In
+    // stage 1, the title item content fades in. In stage 2, the background of the option items
+    // fades in. In stage 3, the option item content fades in.
+    private static final int SHOW_MENU_STAGES_COUNT = 3;
 
     public BubbleBarMenuView(Context context) {
         this(context, null /* attrs */);
@@ -97,6 +101,35 @@
         }
     }
 
+    /** Animates the menu from the specified start scale. */
+    public void animateFromStartScale(float currentScale, float progress) {
+        int menuItemElevation = getResources().getDimensionPixelSize(
+                R.dimen.bubble_manage_menu_elevation);
+        setScaleX(currentScale);
+        setScaleY(currentScale);
+        setAlphaForTitleViews(progress);
+        mBubbleSectionView.setElevation(menuItemElevation * progress);
+        float actionsBackgroundAlpha = Math.max(0,
+                (progress - (float) 1 / SHOW_MENU_STAGES_COUNT) * (SHOW_MENU_STAGES_COUNT - 1));
+        float actionItemsAlpha = Math.max(0,
+                (progress - (float) 2 / SHOW_MENU_STAGES_COUNT) * SHOW_MENU_STAGES_COUNT);
+        mActionsSectionView.setAlpha(actionsBackgroundAlpha);
+        mActionsSectionView.setElevation(menuItemElevation * actionsBackgroundAlpha);
+        setMenuItemViewsAlpha(actionItemsAlpha);
+    }
+
+    private void setAlphaForTitleViews(float alpha) {
+        mBubbleIconView.setAlpha(alpha);
+        mBubbleTitleView.setAlpha(alpha);
+        mBubbleDismissIconView.setAlpha(alpha);
+    }
+
+    private void setMenuItemViewsAlpha(float alpha) {
+        for (int i = mActionsSectionView.getChildCount() - 1; i >= 0; i--) {
+            mActionsSectionView.getChildAt(i).setAlpha(alpha);
+        }
+    }
+
     /** Update menu details with bubble info */
     void updateInfo(Bubble bubble) {
         if (bubble.getIcon() != null) {
@@ -153,6 +186,11 @@
         return mBubbleSectionView.getAlpha();
     }
 
+    /** Return title menu item height. */
+    public float getTitleItemHeight() {
+        return mBubbleSectionView.getHeight();
+    }
+
     /**
      * Menu action details used to create menu items
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
index 5148107..9dd0cae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -15,6 +15,9 @@
  */
 package com.android.wm.shell.bubbles.bar;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -26,13 +29,10 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import androidx.dynamicanimation.animation.DynamicAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
+import com.android.app.animation.Interpolators;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.Bubble;
-import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import java.util.ArrayList;
 
@@ -40,22 +40,26 @@
  * Manages bubble bar expanded view menu presentation and animations
  */
 class BubbleBarMenuViewController {
-    private static final float MENU_INITIAL_SCALE = 0.5f;
+
+    private static final float WIDTH_SWAP_FRACTION = 0.4F;
+    private static final long MENU_ANIMATION_DURATION = 600;
+
     private final Context mContext;
     private final ViewGroup mRootView;
+    private final BubbleBarHandleView mHandleView;
     private @Nullable Listener mListener;
     private @Nullable Bubble mBubble;
     private @Nullable BubbleBarMenuView mMenuView;
     /** A transparent view used to intercept touches to collapse menu when presented */
     private @Nullable View mScrimView;
-    private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator;
-    private PhysicsAnimator.SpringConfig mMenuSpringConfig;
+    private @Nullable ValueAnimator mMenuAnimator;
 
-    BubbleBarMenuViewController(Context context, ViewGroup rootView) {
+
+    BubbleBarMenuViewController(Context context, BubbleBarHandleView handleView,
+            ViewGroup rootView) {
         mContext = context;
         mRootView = rootView;
-        mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
-                SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mHandleView = handleView;
     }
 
     /** Tells if the menu is visible or being animated */
@@ -81,20 +85,21 @@
         if (mMenuView == null || mScrimView == null) {
             setupMenu();
         }
-        cancelAnimations();
-        mMenuView.setVisibility(View.VISIBLE);
-        mScrimView.setVisibility(View.VISIBLE);
-        Runnable endActions = () -> {
-            mMenuView.getChildAt(0).requestAccessibilityFocus();
-            if (mListener != null) {
-                mListener.onMenuVisibilityChanged(true /* isShown */);
+        runOnMenuIsMeasured(() -> {
+            mMenuView.setVisibility(View.VISIBLE);
+            mScrimView.setVisibility(View.VISIBLE);
+            Runnable endActions = () -> {
+                mMenuView.getChildAt(0).requestAccessibilityFocus();
+                if (mListener != null) {
+                    mListener.onMenuVisibilityChanged(true /* isShown */);
+                }
+            };
+            if (animated) {
+                animateTransition(true /* show */, endActions);
+            } else {
+                endActions.run();
             }
-        };
-        if (animated) {
-            animateTransition(true /* show */, endActions);
-        } else {
-            endActions.run();
-        }
+        });
     }
 
     /**
@@ -103,18 +108,30 @@
      */
     void hideMenu(boolean animated) {
         if (mMenuView == null || mScrimView == null) return;
-        cancelAnimations();
-        Runnable endActions = () -> {
-            mMenuView.setVisibility(View.GONE);
-            mScrimView.setVisibility(View.GONE);
-            if (mListener != null) {
-                mListener.onMenuVisibilityChanged(false /* isShown */);
+        runOnMenuIsMeasured(() -> {
+            Runnable endActions = () -> {
+                mHandleView.restoreAnimationDefaults();
+                mMenuView.setVisibility(View.GONE);
+                mScrimView.setVisibility(View.GONE);
+                mHandleView.setVisibility(View.VISIBLE);
+                if (mListener != null) {
+                    mListener.onMenuVisibilityChanged(false /* isShown */);
+                }
+            };
+            if (animated) {
+                animateTransition(false /* show */, endActions);
+            } else {
+                endActions.run();
             }
-        };
-        if (animated) {
-            animateTransition(false /* show */, endActions);
+        });
+    }
+
+    private void runOnMenuIsMeasured(Runnable action) {
+        if (mMenuView.getWidth() == 0 || mMenuView.getHeight() == 0) {
+            // the menu view is not yet measured, postpone showing the animation
+            mMenuView.post(() -> runOnMenuIsMeasured(action));
         } else {
-            endActions.run();
+            action.run();
         }
     }
 
@@ -125,24 +142,63 @@
      */
     private void animateTransition(boolean show, Runnable endActions) {
         if (mMenuView == null) return;
-        mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
-        mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
-        mMenuAnimator
-                .spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
-                .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
-                .withEndActions(() -> {
-                    mMenuAnimator = null;
-                    endActions.run();
-                })
-                .start();
+        float startValue = show ? 0 : 1;
+        if (mMenuAnimator != null && mMenuAnimator.isRunning()) {
+            startValue = (float) mMenuAnimator.getAnimatedValue();
+            mMenuAnimator.cancel();
+        }
+        ValueAnimator showMenuAnimation = ValueAnimator.ofFloat(startValue, show ? 1 : 0);
+        showMenuAnimation.setDuration(MENU_ANIMATION_DURATION);
+        showMenuAnimation.setInterpolator(Interpolators.EMPHASIZED);
+        showMenuAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mMenuAnimator = null;
+                endActions.run();
+            }
+        });
+        mMenuAnimator = showMenuAnimation;
+        setupAnimatorListener(showMenuAnimation);
+        showMenuAnimation.start();
     }
 
-    /** Cancel running animations */
-    private void cancelAnimations() {
-        if (mMenuAnimator != null) {
-            mMenuAnimator.cancel();
-            mMenuAnimator = null;
+    /** Setup listener that orchestrates the animation. */
+    private void setupAnimatorListener(ValueAnimator showMenuAnimation) {
+        // Getting views properties start values
+        int widthDiff = mMenuView.getWidth() - mHandleView.getHandleWidth();
+        int handleHeight = mHandleView.getHandleHeight();
+        float targetWidth = mHandleView.getHandleWidth() + widthDiff * WIDTH_SWAP_FRACTION;
+        float targetHeight = targetWidth * mMenuView.getTitleItemHeight() / mMenuView.getWidth();
+        int menuColor;
+        try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{
+                com.android.internal.R.attr.materialColorSurfaceBright,
+        })) {
+            menuColor = ta.getColor(0, Color.WHITE);
         }
+        // Calculating deltas
+        float swapScale = targetWidth / mMenuView.getWidth();
+        float handleWidthDelta = targetWidth - mHandleView.getHandleWidth();
+        float handleHeightDelta = targetHeight - handleHeight;
+        // Setting update listener that will orchestrate the animation
+        showMenuAnimation.addUpdateListener(animator -> {
+            float animationProgress = (float) animator.getAnimatedValue();
+            boolean showHandle = animationProgress <= WIDTH_SWAP_FRACTION;
+            mHandleView.setVisibility(showHandle ? View.VISIBLE : View.GONE);
+            mMenuView.setVisibility(showHandle ? View.GONE : View.VISIBLE);
+            if (showHandle) {
+                float handleAnimationProgress = animationProgress / WIDTH_SWAP_FRACTION;
+                mHandleView.animateHandleForMenu(handleAnimationProgress, handleWidthDelta,
+                        handleHeightDelta, menuColor);
+            } else {
+                mMenuView.setTranslationY(mHandleView.getHandlePaddingTop());
+                mMenuView.setPivotY(0);
+                mMenuView.setPivotX((float) mMenuView.getWidth() / 2);
+                float menuAnimationProgress =
+                        (animationProgress - WIDTH_SWAP_FRACTION) / (1 - WIDTH_SWAP_FRACTION);
+                float currentMenuScale = swapScale + (1 - swapScale) * menuAnimationProgress;
+                mMenuView.animateFromStartScale(currentMenuScale, menuAnimationProgress);
+            }
+        });
     }
 
     /** Sets up and inflate menu views */
@@ -150,9 +206,6 @@
         // Menu view setup
         mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
                 R.layout.bubble_bar_menu_view, mRootView, false);
-        mMenuView.setAlpha(0f);
-        mMenuView.setPivotY(0f);
-        mMenuView.setScaleY(MENU_INITIAL_SCALE);
         mMenuView.setOnCloseListener(() -> hideMenu(true  /* animated */));
         if (mBubble != null) {
             mMenuView.updateInfo(mBubble);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
index 4abb35c..193c593 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt
@@ -16,8 +16,11 @@
 package com.android.wm.shell.common.pip
 
 import android.app.AppOpsManager
+import android.content.ComponentName
 import android.content.Context
 import android.content.pm.PackageManager
+import android.util.Pair
+import com.android.internal.annotations.VisibleForTesting
 import com.android.wm.shell.common.ShellExecutor
 
 class PipAppOpsListener(
@@ -27,10 +30,12 @@
 ) {
     private val mAppOpsManager: AppOpsManager = checkNotNull(
         mContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager)
+    private var mTopPipActivityInfoSupplier: (Context) -> Pair<ComponentName?, Int> =
+        PipUtils::getTopPipActivity
     private val mAppOpsChangedListener = AppOpsManager.OnOpChangedListener { _, packageName ->
         try {
             // Dismiss the PiP once the user disables the app ops setting for that package
-            val topPipActivityInfo = PipUtils.getTopPipActivity(mContext)
+            val topPipActivityInfo = mTopPipActivityInfoSupplier.invoke(mContext)
             val componentName = topPipActivityInfo.first ?: return@OnOpChangedListener
             val userId = topPipActivityInfo.second
             val appInfo = mContext.packageManager
@@ -75,4 +80,9 @@
         /** Dismisses the PIP window.  */
         fun dismissPip()
     }
+
+    @VisibleForTesting
+    fun setTopPipActivityInfoSupplier(supplier: (Context) -> Pair<ComponentName?, Int>) {
+        mTopPipActivityInfoSupplier = supplier
+    }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index a472f79..601cf70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -67,6 +67,7 @@
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopBackNavigationTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler;
 import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
@@ -836,14 +837,21 @@
     @Provides
     static Optional<DesktopImmersiveController> provideDesktopImmersiveController(
             Context context,
+            ShellInit shellInit,
             Transitions transitions,
             @DynamicOverride DesktopRepository desktopRepository,
             DisplayController displayController,
-            ShellTaskOrganizer shellTaskOrganizer) {
+            ShellTaskOrganizer shellTaskOrganizer,
+            ShellCommandHandler shellCommandHandler) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return Optional.of(
                     new DesktopImmersiveController(
-                            transitions, desktopRepository, displayController, shellTaskOrganizer));
+                            shellInit,
+                            transitions,
+                            desktopRepository,
+                            displayController,
+                            shellTaskOrganizer,
+                            shellCommandHandler));
         }
         return Optional.empty();
     }
@@ -908,6 +916,16 @@
 
     @WMSingleton
     @Provides
+    static DesktopBackNavigationTransitionHandler provideDesktopBackNavigationTransitionHandler(
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellAnimationThread ShellExecutor animExecutor,
+            DisplayController displayController) {
+        return new DesktopBackNavigationTransitionHandler(mainExecutor, animExecutor,
+                displayController);
+    }
+
+    @WMSingleton
+    @Provides
     static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler(
             Transitions transitions) {
         return new DesktopModeDragAndDropTransitionHandler(transitions);
@@ -957,6 +975,7 @@
             Optional<DesktopRepository> desktopRepository,
             Transitions transitions,
             ShellTaskOrganizer shellTaskOrganizer,
+            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
             ShellInit shellInit) {
         return desktopRepository.flatMap(
                 repository ->
@@ -966,6 +985,7 @@
                                         repository,
                                         transitions,
                                         shellTaskOrganizer,
+                                        desktopMixedTransitionHandler.get(),
                                         shellInit)));
     }
 
@@ -978,6 +998,7 @@
             FreeformTaskTransitionHandler freeformTaskTransitionHandler,
             CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
             Optional<DesktopImmersiveController> desktopImmersiveController,
+            DesktopBackNavigationTransitionHandler desktopBackNavigationTransitionHandler,
             InteractionJankMonitor interactionJankMonitor,
             @ShellMainThread Handler handler,
             ShellInit shellInit,
@@ -994,6 +1015,7 @@
                         freeformTaskTransitionHandler,
                         closeDesktopTaskTransitionHandler,
                         desktopImmersiveController.get(),
+                        desktopBackNavigationTransitionHandler,
                         interactionJankMonitor,
                         handler,
                         shellInit,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 3a4764d..3cd5df3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.os.Handler;
 
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.common.DisplayController;
@@ -41,6 +42,7 @@
 import com.android.wm.shell.common.pip.SizeSpecSource;
 import com.android.wm.shell.dagger.WMShellBaseModule;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.PipAnimationController;
 import com.android.wm.shell.pip.PipParamsChangedForwarder;
@@ -169,6 +171,8 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             Optional<SplitScreenController> splitScreenControllerOptional,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
+            Optional<DesktopRepository> desktopRepositoryOptional,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             DisplayController displayController,
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
@@ -176,7 +180,8 @@
                 syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState,
                 pipBoundsAlgorithm, menuPhoneController, pipAnimationController,
                 pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
-                splitScreenControllerOptional, pipPerfHintControllerOptional, displayController,
+                splitScreenControllerOptional, pipPerfHintControllerOptional,
+                desktopRepositoryOptional, rootTaskDisplayAreaOrganizer, displayController,
                 pipUiEventLogger, shellTaskOrganizer, mainExecutor);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index 8d1b15c1..78e676f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.common.DisplayController;
@@ -214,6 +215,7 @@
             PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
             Optional<SplitScreenController> splitScreenControllerOptional,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             DisplayController displayController,
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
@@ -221,8 +223,9 @@
                 syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState,
                 tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
                 pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
-                splitScreenControllerOptional, pipPerfHintControllerOptional, displayController,
-                pipUiEventLogger, shellTaskOrganizer, mainExecutor);
+                splitScreenControllerOptional, pipPerfHintControllerOptional,
+                rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger,
+                shellTaskOrganizer, mainExecutor);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt
new file mode 100644
index 0000000..83b0f84
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.animation.Animator
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.DisplayMetrics
+import android.view.SurfaceControl.Transaction
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.shared.animation.MinimizeAnimator.create
+import com.android.wm.shell.transition.Transitions
+
+/**
+ * The [Transitions.TransitionHandler] that handles transitions for tasks that are closing or going
+ * to back as part of back navigation. This handler is used only for animating transitions.
+ */
+class DesktopBackNavigationTransitionHandler(
+    private val mainExecutor: ShellExecutor,
+    private val animExecutor: ShellExecutor,
+    private val displayController: DisplayController,
+) : Transitions.TransitionHandler {
+
+    /** Shouldn't handle anything */
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo,
+    ): WindowContainerTransaction? = null
+
+    /** Animates a transition with minimizing tasks */
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ): Boolean {
+        if (!TransitionUtil.isClosingType(info.type)) return false
+
+        val animations = mutableListOf<Animator>()
+        val onAnimFinish: (Animator) -> Unit = { animator ->
+            mainExecutor.execute {
+                // Animation completed
+                animations.remove(animator)
+                if (animations.isEmpty()) {
+                    // All animations completed, finish the transition
+                    finishCallback.onTransitionFinished(/* wct= */ null)
+                }
+            }
+        }
+
+        animations +=
+            info.changes
+                .filter {
+                    it.mode == info.type &&
+                            it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+                }
+                .mapNotNull { createMinimizeAnimation(it, finishTransaction, onAnimFinish) }
+        if (animations.isEmpty()) return false
+        animExecutor.execute { animations.forEach(Animator::start) }
+        return true
+    }
+
+    private fun createMinimizeAnimation(
+        change: TransitionInfo.Change,
+        finishTransaction: Transaction,
+        onAnimFinish: (Animator) -> Unit
+    ): Animator? {
+        val t = Transaction()
+        val sc = change.leash
+        finishTransaction.hide(sc)
+        val displayMetrics: DisplayMetrics? =
+            change.taskInfo?.let {
+                displayController.getDisplayContext(it.displayId)?.getResources()?.displayMetrics
+            }
+        return displayMetrics?.let { create(it, change, t, onAnimFinish) }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index f69aa6d..1acde73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -34,10 +34,13 @@
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.transition.Transitions.TransitionObserver
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
+import java.io.PrintWriter
 
 /**
  * A controller to move tasks in/out of desktop's full immersive state where the task
@@ -45,27 +48,34 @@
  * be transient below the status bar like in fullscreen immersive mode.
  */
 class DesktopImmersiveController(
+    shellInit: ShellInit,
     private val transitions: Transitions,
     private val desktopRepository: DesktopRepository,
     private val displayController: DisplayController,
     private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val shellCommandHandler: ShellCommandHandler,
     private val transactionSupplier: () -> SurfaceControl.Transaction,
 ) : TransitionHandler, TransitionObserver {
 
     constructor(
+        shellInit: ShellInit,
         transitions: Transitions,
         desktopRepository: DesktopRepository,
         displayController: DisplayController,
         shellTaskOrganizer: ShellTaskOrganizer,
+        shellCommandHandler: ShellCommandHandler,
     ) : this(
+        shellInit,
         transitions,
         desktopRepository,
         displayController,
         shellTaskOrganizer,
+        shellCommandHandler,
         { SurfaceControl.Transaction() }
     )
 
-    private var state: TransitionState? = null
+    @VisibleForTesting
+    var state: TransitionState? = null
 
     @VisibleForTesting
     val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>()
@@ -79,10 +89,21 @@
     /** A listener to invoke on animation changes during entry/exit. */
     var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
 
+    init {
+        shellInit.addInitCallback({ onInit() }, this)
+    }
+
+    fun onInit() {
+        shellCommandHandler.addDumpCallback(this::dump, this)
+    }
+
     /** Starts a transition to enter full immersive state inside the desktop. */
     fun moveTaskToImmersive(taskInfo: RunningTaskInfo) {
         if (inProgress) {
-            logV("Cannot start entry because transition already in progress.")
+            logV(
+                "Cannot start entry because transition(s) already in progress: %s",
+                getRunningTransitions()
+            )
             return
         }
         val wct = WindowContainerTransaction().apply {
@@ -100,7 +121,10 @@
 
     fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
         if (inProgress) {
-            logV("Cannot start exit because transition already in progress.")
+            logV(
+                "Cannot start exit because transition(s) already in progress: %s",
+                getRunningTransitions()
+            )
             return
         }
 
@@ -225,14 +249,19 @@
         finishCallback: Transitions.TransitionFinishCallback
     ): Boolean {
         val state = requireState()
-        if (transition != state.transition) return false
+        check(state.transition == transition) {
+            "Transition $transition did not match expected state=$state"
+        }
         logD("startAnimation transition=%s", transition)
         animateResize(
             targetTaskId = state.taskId,
             info = info,
             startTransaction = startTransaction,
             finishTransaction = finishTransaction,
-            finishCallback = finishCallback
+            finishCallback = {
+                finishCallback.onTransitionFinished(/* wct= */ null)
+                clearState()
+            },
         )
         return true
     }
@@ -242,12 +271,18 @@
         info: TransitionInfo,
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction,
-        finishCallback: Transitions.TransitionFinishCallback
+        finishCallback: Transitions.TransitionFinishCallback,
     ) {
         logD("animateResize for task#%d", targetTaskId)
-        val change = info.changes.first { c ->
+        val change = info.changes.firstOrNull { c ->
             val taskInfo = c.taskInfo
-            return@first taskInfo != null && taskInfo.taskId == targetTaskId
+            return@firstOrNull taskInfo != null && taskInfo.taskId == targetTaskId
+        }
+        if (change == null) {
+            logD("Did not find change for task#%d to animate", targetTaskId)
+            startTransaction.apply()
+            finishCallback.onTransitionFinished(/* wct= */ null)
+            return
         }
         animateResizeChange(change, startTransaction, finishTransaction, finishCallback)
     }
@@ -288,7 +323,6 @@
                         .apply()
                     onTaskResizeAnimationListener?.onAnimationEnd(taskId)
                     finishCallback.onTransitionFinished(null /* wct */)
-                    clearState()
                 }
             )
             addUpdateListener { animation ->
@@ -357,8 +391,17 @@
         // Check if this is a direct immersive enter/exit transition.
         if (transition == state?.transition) {
             val state = requireState()
-            val startBounds = info.changes.first { c -> c.taskInfo?.taskId == state.taskId }
-                .startAbsBounds
+            val immersiveChange = info.changes.firstOrNull { c ->
+                c.taskInfo?.taskId == state.taskId
+            }
+            if (immersiveChange == null) {
+                logV(
+                    "Direct move for task#%d in %s direction missing immersive change.",
+                    state.taskId, state.direction
+                )
+                return
+            }
+            val startBounds = immersiveChange.startAbsBounds
             logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction)
             when (state.direction) {
                 Direction.ENTER -> {
@@ -446,11 +489,30 @@
     private fun requireState(): TransitionState =
         state ?: error("Expected non-null transition state")
 
+    private fun getRunningTransitions(): List<IBinder> {
+        val running = mutableListOf<IBinder>()
+        state?.let {
+            running.add(it.transition)
+        }
+        pendingExternalExitTransitions.forEach {
+            running.add(it.transition)
+        }
+        return running
+    }
+
     private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean =
         changes.any { c -> c.taskInfo?.taskId == taskId }
 
+    private fun dump(pw: PrintWriter, prefix: String) {
+        val innerPrefix = "$prefix  "
+        pw.println("${prefix}DesktopImmersiveController")
+        pw.println(innerPrefix + "state=" + state)
+        pw.println(innerPrefix + "pendingExternalExitTransitions=" + pendingExternalExitTransitions)
+    }
+
     /** The state of the currently running transition. */
-    private data class TransitionState(
+    @VisibleForTesting
+    data class TransitionState(
         val transition: IBinder,
         val displayId: Int,
         val taskId: Int,
@@ -483,7 +545,8 @@
         fun asExit(): Exit? = if (this is Exit) this else null
     }
 
-    private enum class Direction {
+    @VisibleForTesting
+    enum class Direction {
         ENTER, EXIT
     }
 
@@ -495,9 +558,10 @@
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
 
-    private companion object {
+    companion object {
         private const val TAG = "DesktopImmersive"
 
-        private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
+        @VisibleForTesting
+        const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index cefcb75..2001f97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -52,6 +52,7 @@
     private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
     private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
     private val desktopImmersiveController: DesktopImmersiveController,
+    private val desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler,
     private val interactionJankMonitor: InteractionJankMonitor,
     @ShellMainThread private val handler: Handler,
     shellInit: ShellInit,
@@ -161,6 +162,14 @@
                 finishTransaction,
                 finishCallback
             )
+            is PendingMixedTransition.Minimize -> animateMinimizeTransition(
+                pending,
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
         }
     }
 
@@ -205,11 +214,6 @@
         finishTransaction: SurfaceControl.Transaction,
         finishCallback: TransitionFinishCallback,
     ): Boolean {
-        val launchChange = findDesktopTaskChange(info, pending.launchingTask)
-        if (launchChange == null) {
-            logV("No launch Change, returning")
-            return false
-        }
         // Check if there's also an immersive change during this launch.
         val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask ->
             findDesktopTaskChange(info, exitingTask)
@@ -217,6 +221,13 @@
         val minimizeChange = pending.minimizingTask?.let { minimizingTask ->
             findDesktopTaskChange(info, minimizingTask)
         }
+        val launchChange = findDesktopTaskChange(info, pending.launchingTask)
+        if (launchChange == null) {
+            check(minimizeChange == null)
+            check(immersiveExitChange == null)
+            logV("No launch Change, returning")
+            return false
+        }
 
         var subAnimationCount = -1
         var combinedWct: WindowContainerTransaction? = null
@@ -270,6 +281,42 @@
         )
     }
 
+    private fun animateMinimizeTransition(
+        pending: PendingMixedTransition.Minimize,
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback,
+    ): Boolean {
+        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false
+
+        val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask)
+        if (minimizeChange == null) {
+            logW("Should have minimizing desktop task")
+            return false
+        }
+        if (pending.isLastTask) {
+            // Dispatch close desktop task animation to the default transition handlers.
+            return dispatchToLeftoverHandler(
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
+        }
+
+        // Animate minimizing desktop task transition with [DesktopBackNavigationTransitionHandler].
+        return desktopBackNavigationTransitionHandler.startAnimation(
+            transition,
+            info,
+            startTransaction,
+            finishTransaction,
+            finishCallback,
+        )
+    }
+
     override fun onTransitionConsumed(
         transition: IBinder,
         aborted: Boolean,
@@ -398,6 +445,14 @@
             val minimizingTask: Int?,
             val exitingImmersiveTask: Int?,
         ) : PendingMixedTransition()
+
+        /** A task is minimizing. This should be used for task going to back and some closing cases
+         * with back navigation. */
+        data class Minimize(
+            override val transition: IBinder,
+            val minimizingTask: Int,
+            val isLastTask: Boolean,
+        ) : PendingMixedTransition()
     }
 
     private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index bed484c..39586e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -594,6 +594,10 @@
                 FrameworkStatsLog
                     .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER
             ),
+            DRAG_TO_TOP_RESIZE_TRIGGER(
+                FrameworkStatsLog
+                    .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__DRAG_TO_TOP_RESIZE_TRIGGER
+            ),
         }
 
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index fda709a..08ca55f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -102,6 +102,9 @@
     /* Tracks last bounds of task before toggled to stable bounds. */
     private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>()
 
+    /* Tracks last bounds of task before it is minimized. */
+    private val boundsBeforeMinimizeByTaskId = SparseArray<Rect>()
+
     /* Tracks last bounds of task before toggled to immersive state. */
     private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
 
@@ -462,6 +465,14 @@
     fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) =
         boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
 
+    /** Removes and returns the bounds saved before minimizing the given task. */
+    fun removeBoundsBeforeMinimize(taskId: Int): Rect? =
+        boundsBeforeMinimizeByTaskId.removeReturnOld(taskId)
+
+    /** Saves the bounds of the given task before minimizing. */
+    fun saveBoundsBeforeMinimize(taskId: Int, bounds: Rect?) =
+        boundsBeforeMinimizeByTaskId.set(taskId, Rect(bounds))
+
     /** Removes and returns the bounds saved before entering immersive with the given task. */
     fun removeBoundsBeforeFullImmersive(taskId: Int): Rect? =
         boundsBeforeFullImmersiveByTaskId.removeReturnOld(taskId)
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 162879c..223038f 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
@@ -871,11 +871,10 @@
             return
         }
 
-        // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top.
         desktopModeEventLogger.logTaskResizingStarted(
-            ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
+            ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
         )
-        toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent)
+        toggleDesktopTaskSize(taskInfo, ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent)
     }
 
     private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
@@ -1291,7 +1290,11 @@
                     // Check if freeform task launch during recents should be handled
                     shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
                     // Check if the closing task needs to be handled
-                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
+                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(
+                        task,
+                        transition,
+                        request.type
+                    )
                     // Check if the top task shouldn't be allowed to enter desktop mode
                     isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task)
                     // Check if fullscreen task should be updated
@@ -1621,7 +1624,7 @@
     }
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
-    private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+    private fun handleTaskClosing(task: RunningTaskInfo, transition: IBinder, requestType: Int): WindowContainerTransaction? {
         logV("handleTaskClosing")
         if (!isDesktopModeShowing(task.displayId))
             return null
@@ -1637,8 +1640,15 @@
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
             desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+        } else if (requestType == TRANSIT_CLOSE) {
+            // Handle closing tasks, tasks that are going to back are handled in
+            // [DesktopTasksTransitionObserver].
+            desktopMixedTransitionHandler.addPendingMixedTransition(
+                DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
+                    transition, task.taskId, taskRepository.getVisibleTaskCount(task.displayId) == 1
+                )
+            )
         }
-
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(
                 task.displayId,
@@ -1770,9 +1780,13 @@
         transition: IBinder,
         taskIdToMinimize: Int,
     ) {
-        val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) ?: return
+        val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize)
         desktopTasksLimiter.ifPresent {
-            it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId)
+            it.addPendingMinimizeChange(
+                transition = transition,
+                displayId = taskToMinimize?.displayId ?: DEFAULT_DISPLAY,
+                taskId = taskIdToMinimize
+            )
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index f0e3a2b..77af627 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -92,6 +92,12 @@
             }
             taskToMinimize.transitionInfo = info
             activeTransitionTokensAndTasks[transition] = taskToMinimize
+
+            // Save current bounds before minimizing in case we need to restore to it later.
+            val boundsBeforeMinimize = info.changes.find { change ->
+                change.taskInfo?.taskId == taskToMinimize.taskId }?.startAbsBounds
+            taskRepository.saveBoundsBeforeMinimize(taskToMinimize.taskId, boundsBeforeMinimize)
+
             this@DesktopTasksLimiter.minimizeTask(
                     taskToMinimize.displayId, taskToMinimize.taskId)
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index d1534da..c39c715 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -46,6 +46,7 @@
     private val desktopRepository: DesktopRepository,
     private val transitions: Transitions,
     private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
     shellInit: ShellInit
 ) : Transitions.TransitionObserver {
 
@@ -71,7 +72,7 @@
         // TODO: b/332682201 Update repository state
         updateWallpaperToken(info)
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
-            handleBackNavigation(info)
+            handleBackNavigation(transition, info)
             removeTaskIfNeeded(info)
         }
         removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
@@ -95,7 +96,7 @@
         }
     }
 
-    private fun handleBackNavigation(info: TransitionInfo) {
+    private fun handleBackNavigation(transition: IBinder, info: TransitionInfo) {
         // When default back navigation happens, transition type is TO_BACK and the change is
         // TO_BACK. Mark the task going to back as minimized.
         if (info.type == TRANSIT_TO_BACK) {
@@ -105,10 +106,14 @@
                     continue
                 }
 
-                if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+                val visibleTaskCount = desktopRepository.getVisibleTaskCount(taskInfo.displayId)
+                if (visibleTaskCount > 0 &&
                     change.mode == TRANSIT_TO_BACK &&
                     taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
                     desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+                    desktopMixedTransitionHandler.addPendingMixedTransition(
+                        DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
+                            transition, taskInfo.taskId, visibleTaskCount == 1))
                 }
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index b27c428..0154d04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -45,12 +45,17 @@
     }
 
     /**
-     * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
+     * Set the callback when isInPip state is changed.
      *
-     * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
-     *                 when it's changed.
+     * @param callback The callback accepts the state of isInPip when it's changed.
      */
-    default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
+    default void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {}
+
+    /**
+     * Remove the callback when isInPip state is changed.
+     * @param callback The callback accepts the state of isInPip when it's changed.
+     */
+    default void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {}
 
     /**
      * Called when showing Pip menu.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4aeecbe..5276d9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -27,6 +27,7 @@
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.app.AppCompatTaskInfo;
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -176,12 +177,12 @@
     public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash,
             Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect,
             @PipAnimationController.TransitionDirection int direction, float startingAngle,
-            @Surface.Rotation int rotationDelta) {
+            @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
         if (mCurrentAnimator == null) {
             mCurrentAnimator = setupPipTransitionAnimator(
                     PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds,
                             endBounds, sourceHintRect, direction, 0 /* startingAngle */,
-                            rotationDelta));
+                            rotationDelta, alwaysAnimateTaskBounds));
         } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
                 && mCurrentAnimator.isRunning()) {
             // If we are still animating the fade into pip, then just move the surface and ensure
@@ -197,7 +198,8 @@
             mCurrentAnimator.cancel();
             mCurrentAnimator = setupPipTransitionAnimator(
                     PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds,
-                            endBounds, sourceHintRect, direction, startingAngle, rotationDelta));
+                            endBounds, sourceHintRect, direction, startingAngle, rotationDelta,
+                            alwaysAnimateTaskBounds));
         }
         return mCurrentAnimator;
     }
@@ -585,28 +587,32 @@
         static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash,
                 Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint,
                 @PipAnimationController.TransitionDirection int direction, float startingAngle,
-                @Surface.Rotation int rotationDelta) {
+                @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
             final boolean isOutPipDirection = isOutPipDirection(direction);
             final boolean isInPipDirection = isInPipDirection(direction);
             // Just for simplicity we'll interpolate between the source rect hint insets and empty
             // insets to calculate the window crop
             final Rect initialSourceValue;
             final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame;
-            final boolean hasNonMatchFrame = mainWindowFrame != null;
+            final AppCompatTaskInfo compatInfo = taskInfo.appCompatTaskInfo;
+            final boolean isSizeCompatOrLetterboxed = compatInfo.isTopActivityInSizeCompat()
+                    || compatInfo.isTopActivityLetterboxed();
+            // For the animation to swipe PIP to home or restore a PIP task from home, we don't
+            // override to the main window frame since we should animate the whole task.
+            final boolean shouldUseMainWindowFrame = mainWindowFrame != null
+                    && !alwaysAnimateTaskBounds && !isSizeCompatOrLetterboxed;
             final boolean changeOrientation =
                     rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270;
             final Rect baseBounds = new Rect(baseValue);
             final Rect startBounds = new Rect(startValue);
             final Rect endBounds = new Rect(endValue);
             if (isOutPipDirection) {
-                // TODO(b/356277166): handle rotation change with activity that provides main window
-                //  frame.
-                if (hasNonMatchFrame && !changeOrientation) {
+                if (shouldUseMainWindowFrame && !changeOrientation) {
                     endBounds.set(mainWindowFrame);
                 }
                 initialSourceValue = new Rect(endBounds);
             } else if (isInPipDirection) {
-                if (hasNonMatchFrame) {
+                if (shouldUseMainWindowFrame) {
                     baseBounds.set(mainWindowFrame);
                     if (startValue.equals(baseValue)) {
                         // If the start value is at initial state as in PIP animation, also override
@@ -635,9 +641,19 @@
             if (changeOrientation) {
                 lastEndRect = new Rect(endBounds);
                 rotatedEndRect = new Rect(endBounds);
-                // Rotate the end bounds according to the rotation delta because the display will
-                // be rotated to the same orientation.
-                rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
+                // TODO(b/375977163): polish the animation to restoring the PIP task back from
+                //  swipe-pip-to-home. Ideally we should send the transitionInfo after reparenting
+                //  the PIP activity back to the original task.
+                if (shouldUseMainWindowFrame) {
+                    // If we should animate the main window frame, set it to the rotatedRect
+                    // instead. The end bounds reported by transitionInfo is the bounds before
+                    // rotation, while main window frame is calculated after the rotation.
+                    rotatedEndRect.set(mainWindowFrame);
+                } else {
+                    // Rotate the end bounds according to the rotation delta because the display
+                    // will be rotated to the same orientation.
+                    rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
+                }
                 // Use the rect that has the same orientation as the hint rect.
                 initialContainerRect = isOutPipDirection ? rotatedEndRect : initialSourceValue;
             } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index c4e63df..30f1948 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.pip;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -67,6 +68,7 @@
 import android.view.Display;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
 import android.window.TaskOrganizer;
 import android.window.TaskSnapshot;
 import android.window.WindowContainerToken;
@@ -74,7 +76,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLog;
+import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ScreenshotUtils;
@@ -87,6 +91,7 @@
 import com.android.wm.shell.common.pip.PipPerfHintController;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.Interpolators;
@@ -145,6 +150,8 @@
     private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
     private final Optional<SplitScreenController> mSplitScreenOptional;
     @Nullable private final PipPerfHintController mPipPerfHintController;
+    private final Optional<DesktopRepository> mDesktopRepositoryOptional;
+    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     protected final ShellTaskOrganizer mTaskOrganizer;
     protected final ShellExecutor mMainExecutor;
 
@@ -388,6 +395,8 @@
             @NonNull PipParamsChangedForwarder pipParamsChangedForwarder,
             Optional<SplitScreenController> splitScreenOptional,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
+            Optional<DesktopRepository> desktopRepositoryOptional,
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             @NonNull DisplayController displayController,
             @NonNull PipUiEventLogger pipUiEventLogger,
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
@@ -414,6 +423,8 @@
                 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
         mSplitScreenOptional = splitScreenOptional;
         mPipPerfHintController = pipPerfHintControllerOptional.orElse(null);
+        mDesktopRepositoryOptional = desktopRepositoryOptional;
+        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
         mTaskOrganizer = shellTaskOrganizer;
         mMainExecutor = mainExecutor;
 
@@ -741,10 +752,23 @@
     }
 
     /** Returns the bounds to restore to when exiting PIP mode. */
+    // TODO(b/377581840): Instead of manually tracking bounds, use bounds from Core.
     public Rect getExitDestinationBounds() {
+        if (isPipLaunchedInDesktopMode()) {
+            final Rect freeformBounds = mDesktopRepositoryOptional.get().removeBoundsBeforeMinimize(
+                    mTaskInfo.taskId);
+            return Objects.requireNonNullElseGet(freeformBounds, mPipBoundsState::getDisplayBounds);
+        }
         return mPipBoundsState.getDisplayBounds();
     }
 
+    /** Returns whether PiP was launched while in desktop mode. */
+    // TODO(377581840): Update this check to include non-minimized cases, e.g. split to PiP etc.
+    private boolean isPipLaunchedInDesktopMode() {
+        return Flags.enableDesktopWindowingPip() && mDesktopRepositoryOptional.isPresent()
+                && mDesktopRepositoryOptional.get().isMinimizedTask(mTaskInfo.taskId);
+    }
+
     private void exitLaunchIntoPipTask(WindowContainerTransaction wct) {
         wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */);
         mTaskOrganizer.applyTransaction(wct);
@@ -1808,7 +1832,25 @@
      * and can be overridden to restore to an alternate windowing mode.
      */
     public int getOutPipWindowingMode() {
-        // By default, simply reset the windowing mode to undefined.
+        final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+                mTaskInfo.displayId);
+
+        // If PiP was launched while in desktop mode (we should return the task to freeform
+        // windowing mode):
+        // 1) If the display windowing mode is freeform, set windowing mode to undefined so it will
+        //    resolve the windowing mode to the display's windowing mode.
+        // 2) If the display windowing mode is not freeform, set windowing mode to freeform.
+        if (tdaInfo != null && isPipLaunchedInDesktopMode()) {
+            final int displayWindowingMode =
+                    tdaInfo.configuration.windowConfiguration.getWindowingMode();
+            if (displayWindowingMode == WINDOWING_MODE_FREEFORM) {
+                return WINDOWING_MODE_UNDEFINED;
+            } else {
+                return WINDOWING_MODE_FREEFORM;
+            }
+        }
+
+        // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
         return WINDOWING_MODE_UNDEFINED;
     }
 
@@ -1838,9 +1880,11 @@
                 ? mPipBoundsState.getBounds() : currentBounds;
         final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null
                 && mPipAnimationController.getCurrentAnimator().isRunning();
+        // For resize animation, we always animate the whole PIP task bounds.
         final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds,
-                        sourceHintRect, direction, startingAngle, rotationDelta);
+                        sourceHintRect, direction, startingAngle, rotationDelta,
+                        true /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(direction)
                 .setPipTransactionHandler(mPipTransactionHandler)
                 .setDuration(durationMs);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 28b91c6..f7aed44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -530,6 +530,13 @@
                 if (mFixedRotationState != FIXED_ROTATION_TRANSITION
                         && mFinishTransaction != null) {
                     mFinishTransaction.merge(tx);
+                    // Set window crop and position to destination bounds to avoid flickering.
+                    if (hasValidLeash) {
+                        mFinishTransaction.setWindowCrop(leash, destinationBounds.width(),
+                                destinationBounds.height());
+                        mFinishTransaction.setPosition(leash, destinationBounds.left,
+                                destinationBounds.top);
+                    }
                 }
             } else {
                 wct = new WindowContainerTransaction();
@@ -884,7 +891,8 @@
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(taskInfo, pipChange.getLeash(),
                         startBounds, startBounds, endBounds, null, TRANSITION_DIRECTION_LEAVE_PIP,
-                        0 /* startingAngle */, pipRotateDelta);
+                        0 /* startingAngle */, pipRotateDelta,
+                        false /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration)
@@ -899,7 +907,7 @@
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds,
                         endBounds, sourceHintRect, TRANSITION_DIRECTION_LEAVE_PIP,
-                        0 /* startingAngle */, rotationDelta);
+                        0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setDuration(mEnterExitAnimationDuration);
         if (startTransaction != null) {
@@ -1095,8 +1103,6 @@
         if (taskInfo.pictureInPictureParams != null
                 && taskInfo.pictureInPictureParams.isAutoEnterEnabled()
                 && mPipTransitionState.getInSwipePipToHomeTransition()) {
-            // TODO(b/356277166): add support to swipe PIP to home with
-            //  non-match parent activity.
             handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash,
                     sourceHintRect, destinationBounds, taskInfo);
             return;
@@ -1118,7 +1124,7 @@
         if (enterAnimationType == ANIM_TYPE_BOUNDS) {
             animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
                     currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
-                    0 /* startingAngle */, rotationDelta);
+                    0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */);
             if (sourceHintRect == null) {
                 // We use content overlay when there is no source rect hint to enter PiP use bounds
                 // animation. We also temporarily disallow app icon overlay and use color overlay
@@ -1241,10 +1247,14 @@
         // to avoid flicker.
         final Rect savedDisplayCutoutInsets = new Rect(pipTaskInfo.displayCutoutInsets);
         pipTaskInfo.displayCutoutInsets.setEmpty();
+        // Always use the task bounds even if the PIP activity doesn't match parent because the app
+        // and the whole task will move behind. We should animate the whole task bounds in this
+        // case.
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds,
                         destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
-                        0 /* startingAngle */, ROTATION_0 /* rotationDelta */)
+                        0 /* startingAngle */, ROTATION_0 /* rotationDelta */,
+                        true /* alwaysAnimateTaskBounds */)
                         .setPipTransactionHandler(mTransactionConsumer)
                         .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP);
         // The start state is the end state for swipe-auto-pip.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 7f61186..588b887 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -104,6 +104,7 @@
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -215,7 +216,7 @@
 
     private boolean mIsKeyguardShowingOrAnimating;
 
-    private Consumer<Boolean> mOnIsInPipStateChangedListener;
+    private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>();
 
     @VisibleForTesting
     interface PipAnimationListener {
@@ -501,11 +502,11 @@
                     false /* saveRestoreSnapFraction */);
         });
         mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> {
-            if (mOnIsInPipStateChangedListener != null) {
-                final boolean wasInPip = PipTransitionState.isInPip(oldState);
-                final boolean nowInPip = PipTransitionState.isInPip(newState);
-                if (nowInPip != wasInPip) {
-                    mOnIsInPipStateChangedListener.accept(nowInPip);
+            final boolean wasInPip = PipTransitionState.isInPip(oldState);
+            final boolean nowInPip = PipTransitionState.isInPip(newState);
+            if (nowInPip != wasInPip) {
+                for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) {
+                    listener.accept(nowInPip);
                 }
             }
         });
@@ -960,13 +961,19 @@
         mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
     }
 
-    private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
-        mOnIsInPipStateChangedListener = callback;
-        if (mOnIsInPipStateChangedListener != null) {
+    private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+        if (callback != null) {
+            mOnIsInPipStateChangedListeners.add(callback);
             callback.accept(mPipTransitionState.isInPip());
         }
     }
 
+    private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+        if (callback != null) {
+            mOnIsInPipStateChangedListeners.remove(callback);
+        }
+    }
+
     private void setShelfHeightLocked(boolean visible, int height) {
         final int shelfHeight = visible ? height : 0;
         mPipBoundsState.setShelfVisibility(visible, shelfHeight);
@@ -1222,9 +1229,16 @@
         }
 
         @Override
-        public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+        public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
             mMainExecutor.execute(() -> {
-                PipController.this.setOnIsInPipStateChangedListener(callback);
+                PipController.this.addOnIsInPipStateChangedListener(callback);
+            });
+        }
+
+        @Override
+        public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+            mMainExecutor.execute(() -> {
+                PipController.this.removeOnIsInPipStateChangedListener(callback);
             });
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index 614ef2a..fcba461 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -21,6 +21,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
@@ -61,6 +62,7 @@
             @NonNull PipParamsChangedForwarder pipParamsChangedForwarder,
             Optional<SplitScreenController> splitScreenOptional,
             Optional<PipPerfHintController> pipPerfHintControllerOptional,
+            @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             @NonNull DisplayController displayController,
             @NonNull PipUiEventLogger pipUiEventLogger,
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
@@ -68,8 +70,9 @@
         super(context, syncTransactionQueue, pipTransitionState, pipBoundsState,
                 pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController,
                 surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
-                splitScreenOptional, pipPerfHintControllerOptional, displayController,
-                pipUiEventLogger, shellTaskOrganizer, mainExecutor);
+                splitScreenOptional, pipPerfHintControllerOptional, Optional.empty(),
+                rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger,
+                shellTaskOrganizer, mainExecutor);
         mTvPipTransition = tvPipTransition;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index d3f537b..bc09183 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -21,6 +21,7 @@
 
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
@@ -66,6 +67,8 @@
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Consumer;
 
 /**
@@ -94,7 +97,7 @@
     private final PipTouchHandler mPipTouchHandler;
     private final ShellExecutor mMainExecutor;
     private final PipImpl mImpl;
-    private Consumer<Boolean> mOnIsInPipStateChangedListener;
+    private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>();
 
     // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents.
     private PipAnimationListener mPipRecentsAnimationListener;
@@ -413,13 +416,13 @@
                 if (mPipTransitionState.isInSwipePipToHomeTransition()) {
                     mPipTransitionState.resetSwipePipToHomeState();
                 }
-                if (mOnIsInPipStateChangedListener != null) {
-                    mOnIsInPipStateChangedListener.accept(true /* inPip */);
+                for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) {
+                    listener.accept(true /* inPip */);
                 }
                 break;
             case PipTransitionState.EXITED_PIP:
-                if (mOnIsInPipStateChangedListener != null) {
-                    mOnIsInPipStateChangedListener.accept(false /* inPip */);
+                for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) {
+                    listener.accept(false /* inPip */);
                 }
                 break;
         }
@@ -451,13 +454,19 @@
         mPipTransitionState.dump(pw, innerPrefix);
     }
 
-    private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
-        mOnIsInPipStateChangedListener = callback;
-        if (mOnIsInPipStateChangedListener != null) {
+    private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+        if (callback != null) {
+            mOnIsInPipStateChangedListeners.add(callback);
             callback.accept(mPipTransitionState.isInPip());
         }
     }
 
+    private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+        if (callback != null) {
+            mOnIsInPipStateChangedListeners.remove(callback);
+        }
+    }
+
     private void setLauncherAppIconSize(int iconSizePx) {
         mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
     }
@@ -473,9 +482,16 @@
         public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {}
 
         @Override
-        public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+        public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
             mMainExecutor.execute(() -> {
-                PipController.this.setOnIsInPipStateChangedListener(callback);
+                PipController.this.addOnIsInPipStateChangedListener(callback);
+            });
+        }
+
+        @Override
+        public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {
+            mMainExecutor.execute(() -> {
+                PipController.this.removeOnIsInPipStateChangedListener(callback);
             });
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index ea783e9..3caad09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.Surface.ROTATION_0;
 import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_PIP;
@@ -230,6 +231,11 @@
             // If there is no PiP change, exit this transition handler and potentially try others.
             if (pipChange == null) return false;
 
+            // Other targets might have default transforms applied that are not relevant when
+            // playing PiP transitions, so reset those transforms if needed.
+            prepareOtherTargetTransforms(info, startTransaction, finishTransaction);
+
+            // Update the PipTransitionState while supplying the PiP leash and token to be cached.
             Bundle extra = new Bundle();
             extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer());
             extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash());
@@ -341,17 +347,21 @@
                             (destinationBounds.height() - overlaySize) / 2f);
         }
 
-        final int startRotation = pipChange.getStartRotation();
-        final int endRotation = mPipDisplayLayoutState.getRotation();
-        final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
-                : startRotation - endRotation;
+        final int delta = getFixedRotationDelta(info, pipChange);
         if (delta != ROTATION_0) {
-            mPipTransitionState.setInFixedRotation(true);
-            handleBoundsEnterFixedRotation(pipChange, pipActivityChange, endRotation);
+            // Update transition target changes in place to prepare for fixed rotation.
+            handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
         }
 
+        // Update the src-rect-hint in params in place, to set up initial animator transform.
+        Rect sourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange);
+        pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint().set(sourceRectHint);
+
+        // Config-at-end transitions need to have their activities transformed before starting
+        // the animation; this makes the buffer seem like it's been updated to final size.
         prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange,
                 pipActivityChange);
+
         startTransaction.merge(finishTransaction);
         PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
                 startTransaction, finishTransaction, destinationBounds, delta);
@@ -387,55 +397,36 @@
             return false;
         }
 
+        final SurfaceControl pipLeash = getLeash(pipChange);
         final Rect startBounds = pipChange.getStartAbsBounds();
         final Rect endBounds = pipChange.getEndAbsBounds();
-
         final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
-        final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);
-        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
-                endBounds);
-        final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint)
-                : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio);
+        final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange,
+                pipActivityChange);
 
-        final SurfaceControl pipLeash = mPipTransitionState.getPinnedTaskLeash();
-
-        // For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
-        // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
-        // by the Transitions framework to simplify Task opening transitions.
-        if (TransitionUtil.isOpeningType(info.getType())) {
-            for (TransitionInfo.Change change : info.getChanges()) {
-                if (change.getLeash() == null) continue;
-                if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
-                    startTransaction.setAlpha(change.getLeash(), 1f);
-                }
-            }
-        }
-
-        final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
-        final int startRotation = pipChange.getStartRotation();
-        final int endRotation = fixedRotationChange != null
-                ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED;
-        final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
-                : startRotation - endRotation;
-
+        final int delta = getFixedRotationDelta(info, pipChange);
         if (delta != ROTATION_0) {
-            mPipTransitionState.setInFixedRotation(true);
-            handleBoundsEnterFixedRotation(pipChange, pipActivityChange,
-                    fixedRotationChange.getEndFixedRotation());
+            // Update transition target changes in place to prepare for fixed rotation.
+            handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange);
         }
 
         PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
                 startTransaction, finishTransaction, endBounds, delta);
-        if (sourceRectHint == null) {
-            // update the src-rect-hint in params in place, to set up initial animator transform.
-            params.getSourceRectHint().set(adjustedSourceRectHint);
+        if (PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, endBounds) == null) {
+            // If app provided src-rect-hint is invalid, use app icon overlay.
             animator.setAppIconContentOverlay(
                     mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo,
                     mPipBoundsState.getLauncherState().getAppIconSizePx());
         }
 
+        // Update the src-rect-hint in params in place, to set up initial animator transform.
+        params.getSourceRectHint().set(adjustedSourceRectHint);
+
+        // Config-at-end transitions need to have their activities transformed before starting
+        // the animation; this makes the buffer seem like it's been updated to final size.
         prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange,
                 pipActivityChange);
+
         animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange));
         animator.setAnimationEndCallback(() -> {
             if (animator.getContentOverlayLeash() != null) {
@@ -457,11 +448,22 @@
         animator.start();
     }
 
-    private void handleBoundsEnterFixedRotation(TransitionInfo.Change pipTaskChange,
-            TransitionInfo.Change pipActivityChange, int endRotation) {
-        final Rect endBounds = pipTaskChange.getEndAbsBounds();
-        final Rect endActivityBounds = pipActivityChange.getEndAbsBounds();
-        int startRotation = pipTaskChange.getStartRotation();
+    private void handleBoundsEnterFixedRotation(TransitionInfo info,
+            TransitionInfo.Change outPipTaskChange,
+            TransitionInfo.Change outPipActivityChange) {
+        final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+        final Rect endBounds = outPipTaskChange.getEndAbsBounds();
+        final Rect endActivityBounds = outPipActivityChange.getEndAbsBounds();
+        int startRotation = outPipTaskChange.getStartRotation();
+        int endRotation = fixedRotationChange != null
+                ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation();
+
+        if (startRotation == endRotation) {
+            return;
+        }
+
+        // This is used by display change listeners to respond properly to fixed rotation.
+        mPipTransitionState.setInFixedRotation(true);
 
         // Cache the task to activity offset to potentially restore later.
         Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left,
@@ -490,15 +492,15 @@
                 endBounds.top + activityEndOffset.y);
     }
 
-    private void handleExpandFixedRotation(TransitionInfo.Change pipTaskChange, int endRotation) {
-        final Rect endBounds = pipTaskChange.getEndAbsBounds();
+    private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) {
+        final Rect endBounds = outPipTaskChange.getEndAbsBounds();
         final int width = endBounds.width();
         final int height = endBounds.height();
         final int left = endBounds.left;
         final int top = endBounds.top;
         int newTop, newLeft;
 
-        if (endRotation == Surface.ROTATION_90) {
+        if (delta == Surface.ROTATION_90) {
             newLeft = top;
             newTop = -(left + width);
         } else {
@@ -585,15 +587,11 @@
         final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds,
                 startBounds);
 
-        final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
-        final int startRotation = pipChange.getStartRotation();
-        final int endRotation = fixedRotationChange != null
-                ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED;
-        final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
-                : endRotation - startRotation;
-
+        // We define delta = startRotation - endRotation, so we need to flip the sign.
+        final int delta = -getFixedRotationDelta(info, pipChange);
         if (delta != ROTATION_0) {
-            handleExpandFixedRotation(pipChange, endRotation);
+            // Update PiP target change in place to prepare for fixed rotation;
+            handleExpandFixedRotation(pipChange, delta);
         }
 
         PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash,
@@ -661,6 +659,72 @@
         return null;
     }
 
+    @NonNull
+    private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change pipTaskChange,
+            @NonNull TransitionInfo.Change pipActivityChange) {
+        final Rect startBounds = pipTaskChange.getStartAbsBounds();
+        final Rect endBounds = pipTaskChange.getEndAbsBounds();
+        final PictureInPictureParams params = pipTaskChange.getTaskInfo().pictureInPictureParams;
+
+        // Get the source-rect-hint provided by the app and check its validity; null if invalid.
+        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
+                endBounds);
+
+        final Rect adjustedSourceRectHint = new Rect();
+        if (sourceRectHint != null) {
+            adjustedSourceRectHint.set(sourceRectHint);
+            // If multi-activity PiP, use the parent task before PiP to retrieve display cutouts;
+            // then, offset the valid app provided source rect hint by the cutout insets.
+            // For single-activity PiP, just use the pinned task to get the cutouts instead.
+            TransitionInfo.Change parentBeforePip = pipActivityChange.getLastParent() != null
+                    ? getChangeByToken(info, pipActivityChange.getLastParent()) : null;
+            Rect cutoutInsets = parentBeforePip != null
+                    ? parentBeforePip.getTaskInfo().displayCutoutInsets
+                    : pipTaskChange.getTaskInfo().displayCutoutInsets;
+            if (cutoutInsets != null
+                    && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
+                adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
+            }
+        } else {
+            // For non-valid app provided src-rect-hint, calculate one to crop into during
+            // app icon overlay animation.
+            float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);
+            adjustedSourceRectHint.set(
+                    PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio));
+        }
+        return adjustedSourceRectHint;
+    }
+
+    @Surface.Rotation
+    private int getFixedRotationDelta(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change pipChange) {
+        TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info);
+        int startRotation = pipChange.getStartRotation();
+        int endRotation = fixedRotationChange != null
+                ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation();
+        int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0
+                : startRotation - endRotation;
+        return delta;
+    }
+
+    private void prepareOtherTargetTransforms(TransitionInfo info,
+            SurfaceControl.Transaction startTransaction,
+            SurfaceControl.Transaction finishTransaction) {
+        // For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
+        // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
+        // by the Transitions framework to simplify Task opening transitions.
+        if (TransitionUtil.isOpeningType(info.getType())) {
+            for (TransitionInfo.Change change : info.getChanges()) {
+                if (change.getLeash() == null) continue;
+                if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
+                    startTransaction.setAlpha(change.getLeash(), 1f);
+                }
+            }
+        }
+
+    }
+
     private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
         // cache the original task token to check for multi-activity case later
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 40065b9..9016c45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -34,6 +34,8 @@
 
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
 import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION;
 
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -67,6 +69,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.pip.PipUtils;
@@ -216,8 +219,11 @@
                 break;
             }
         }
-        final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
-                mixer == null ? this : mixer);
+        final int transitionType = Flags.enableShellTopTaskTracking()
+                ? TRANSIT_START_RECENTS_TRANSITION
+                : TRANSIT_TO_FRONT;
+        final IBinder transition = mTransitions.startTransition(transitionType,
+                wct, mixer == null ? this : mixer);
         if (mixer != null) {
             setTransitionForMixer.accept(transition);
         }
@@ -300,7 +306,7 @@
                     "RecentsTransitionHandler.mergeAnimation: no controller found");
             return;
         }
-        controller.merge(info, t, finishCallback);
+        controller.merge(info, t, mergeTarget, finishCallback);
     }
 
     @Override
@@ -367,6 +373,8 @@
         private boolean mPausingSeparateHome = false;
         private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null;
         private PictureInPictureSurfaceTransaction mPipTransaction = null;
+        // This is the transition that backs the entire recents transition, and the one that the
+        // pending finish transition below will be merged into
         private IBinder mTransition = null;
         private boolean mKeyguardLocked = false;
         private boolean mWillFinishToHome = false;
@@ -386,6 +394,10 @@
         // next called.
         private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel;
 
+        // Used to track a pending finish transition
+        private IBinder mPendingFinishTransition;
+        private IResultReceiver mPendingRunnerFinishCb;
+
         RecentsController(IRecentsAnimationRunner listener) {
             mInstanceId = System.identityHashCode(this);
             mListener = listener;
@@ -523,6 +535,11 @@
             mInfo = null;
             mTransition = null;
             mPendingPauseSnapshotsForCancel = null;
+            mPipTaskId = -1;
+            mPipTask = null;
+            mPipTransaction = null;
+            mPendingRunnerFinishCb = null;
+            mPendingFinishTransition = null;
             mControllers.remove(this);
             for (int i = 0; i < mStateListeners.size(); i++) {
                 mStateListeners.get(i).onAnimationStateChanged(false);
@@ -734,6 +751,8 @@
                         // the pausing apps.
                         t.setLayer(target.leash, layer);
                     } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                                "  not handling home taskId=%d", taskInfo.taskId);
                         // do nothing
                     } else if (TransitionUtil.isOpeningType(change.getMode())) {
                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -872,16 +891,35 @@
             }
         }
 
+        /**
+         * Note: because we use a book-end transition to finish the recents transition, we must
+         * either always merge the incoming transition, or always cancel the recents transition
+         * if we don't handle the incoming transition to ensure that the end transition is queued
+         * before any unhandled transitions.
+         */
         @SuppressLint("NewApi")
-        void merge(TransitionInfo info, SurfaceControl.Transaction t,
+        void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget,
                 Transitions.TransitionFinishCallback finishCallback) {
             if (mFinishCB == null) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                         "[%d] RecentsController.merge: skip, no finish callback",
                         mInstanceId);
-                // This was no-op'd (likely a repeated start) and we've already sent finish.
+                // This was no-op'd (likely a repeated start) and we've already completed finish.
                 return;
             }
+
+            if (Flags.enableShellTopTaskTracking()
+                    && info.getType() == TRANSIT_END_RECENTS_TRANSITION
+                    && mergeTarget == mTransition) {
+                // This is a pending finish, so merge the end transition to trigger completing the
+                // cleanup of the recents transition
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                        "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION",
+                        mInstanceId);
+                finishCallback.onTransitionFinished(null /* wct */);
+                return;
+            }
+
             if (info.getType() == TRANSIT_SLEEP) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                         "[%d] RecentsController.merge: transit_sleep", mInstanceId);
@@ -1245,7 +1283,8 @@
                 return;
             }
 
-            if (mFinishCB == null) {
+            if (mFinishCB == null
+                    || (Flags.enableShellTopTaskTracking() && mPendingFinishTransition != null)) {
                 Slog.e(TAG, "Duplicate call to finish");
                 return;
             }
@@ -1254,19 +1293,22 @@
                     && !mWillFinishToHome
                     && mPausingTasks != null
                     && mState == STATE_NORMAL;
-            if (returningToApp && allAppsAreTranslucent(mPausingTasks)) {
-                mHomeTransitionObserver.notifyHomeVisibilityChanged(true);
-            } else if (!toHome) {
-                // For some transitions, we may have notified home activity that it became visible.
-                // We need to notify the observer that we are no longer going home.
-                mHomeTransitionObserver.notifyHomeVisibilityChanged(false);
+            if (!Flags.enableShellTopTaskTracking()) {
+                // This is only necessary when the recents transition is finished using a finishWCT,
+                // otherwise a new transition will notify the relevant observers
+                if (returningToApp && allAppsAreTranslucent(mPausingTasks)) {
+                    mHomeTransitionObserver.notifyHomeVisibilityChanged(true);
+                } else if (!toHome) {
+                    // For some transitions, we may have notified home activity that it became
+                    // visible. We need to notify the observer that we are no longer going home.
+                    mHomeTransitionObserver.notifyHomeVisibilityChanged(false);
+                }
             }
+
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                     "[%d] RecentsController.finishInner: toHome=%b userLeave=%b "
-                            + "willFinishToHome=%b state=%d",
-                    mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState);
-            final Transitions.TransitionFinishCallback finishCB = mFinishCB;
-            mFinishCB = null;
+                            + "willFinishToHome=%b state=%d reason=%s",
+                    mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState, reason);
 
             final SurfaceControl.Transaction t = mFinishTransaction;
             final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -1328,6 +1370,7 @@
                 for (int i = 0; i < mClosingTasks.size(); ++i) {
                     cleanUpPausingOrClosingTask(mClosingTasks.get(i), wct, t, sendUserLeaveHint);
                 }
+
                 if (mPipTransaction != null && sendUserLeaveHint) {
                     SurfaceControl pipLeash = null;
                     TransitionInfo.Change pipChange = null;
@@ -1379,15 +1422,50 @@
                             mTransitions.startTransition(TRANSIT_PIP, wct, null /* handler */);
                             // We need to clear the WCT to send finishWCT=null for Recents.
                             wct.clear();
+
+                            if (Flags.enableShellTopTaskTracking()) {
+                                // In this case, we've already started the PIP transition, so we can
+                                // clean up immediately
+                                mPendingRunnerFinishCb = runnerFinishCb;
+                                onFinishInner(null);
+                                return;
+                            }
                         }
                     }
-                    mPipTaskId = -1;
-                    mPipTask = null;
-                    mPipTransaction = null;
                 }
             }
+
+            if (Flags.enableShellTopTaskTracking()) {
+                if (!wct.isEmpty()) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "[%d] RecentsController.finishInner: "
+                                    + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId);
+                    mPendingRunnerFinishCb = runnerFinishCb;
+                    mPendingFinishTransition = mTransitions.startTransition(
+                            TRANSIT_END_RECENTS_TRANSITION, wct,
+                            new PendingFinishTransitionHandler());
+                } else {
+                    // If there's no work to do, just go ahead and clean up
+                    mPendingRunnerFinishCb = runnerFinishCb;
+                    onFinishInner(null /* wct */);
+                }
+            } else {
+                mPendingRunnerFinishCb = runnerFinishCb;
+                onFinishInner(wct);
+            }
+        }
+
+        /**
+         * Runs the actual logic to finish the recents transition.
+         */
+        private void onFinishInner(@Nullable WindowContainerTransaction wct) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.finishInner: Completing finish", mInstanceId);
+            final Transitions.TransitionFinishCallback finishCb = mFinishCB;
+            final IResultReceiver runnerFinishCb = mPendingRunnerFinishCb;
+
             cleanUp();
-            finishCB.onTransitionFinished(wct.isEmpty() ? null : wct);
+            finishCb.onTransitionFinished(wct);
             if (runnerFinishCb != null) {
                 try {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -1472,6 +1550,40 @@
                 }
             });
         }
+
+        /**
+         * A temporary transition handler used with the pending finish transition, which runs the
+         * cleanup/finish logic once the pending transition is merged/handled.
+         * This is only initialized if Flags.enableShellTopTaskTracking() is enabled.
+         */
+        private class PendingFinishTransitionHandler implements Transitions.TransitionHandler {
+            @Override
+            public boolean startAnimation(@NonNull IBinder transition,
+                    @NonNull TransitionInfo info,
+                    @NonNull SurfaceControl.Transaction startTransaction,
+                    @NonNull SurfaceControl.Transaction finishTransaction,
+                    @NonNull Transitions.TransitionFinishCallback finishCallback) {
+                return false;
+            }
+
+            @Nullable
+            @Override
+            public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+                    @NonNull TransitionRequestInfo request) {
+                return null;
+            }
+
+            @Override
+            public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+                    @Nullable SurfaceControl.Transaction finishTransaction) {
+                // Once we have merged (or not if the WCT didn't result in any changes), then we can
+                // run the pending finish logic
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                        "[%d] RecentsController.onTransitionConsumed: "
+                                + "Consumed pending finish transition", mInstanceId);
+                onFinishInner(null /* wct */);
+            }
+        };
     };
 
     /** Utility class to track the state of a task as-seen by recents. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index cc0e1df..19a73f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -55,7 +55,6 @@
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
-import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
@@ -1099,16 +1098,11 @@
 
     void setSideStagePosition(@SplitPosition int sideStagePosition,
             @Nullable WindowContainerTransaction wct) {
-        setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
-    }
-
-    private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
-            @Nullable WindowContainerTransaction wct) {
         if (mSideStagePosition == sideStagePosition) return;
         mSideStagePosition = sideStagePosition;
         sendOnStagePositionChanged();
 
-        if (mSideStage.mVisible && updateBounds) {
+        if (mSideStage.mVisible) {
             if (wct == null) {
                 // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
                 onLayoutSizeChanged(mSplitLayout);
@@ -1199,6 +1193,7 @@
         if (!isSplitActive()) return;
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
+        setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
         applyExitSplitScreen(childrenToTop, wct, exitReason);
     }
 
@@ -1598,6 +1593,13 @@
         }
         if (present) {
             updateRecentTasksSplitPair();
+        } else if (mMainStage.getChildCount() == 0 && mSideStage.getChildCount() == 0) {
+            mRecentTasks.ifPresent(recentTasks -> {
+                // remove the split pair mapping from recentTasks, and disable further updates
+                // to splits in the recents until we enter split again.
+                recentTasks.removeSplitPair(taskId);
+            });
+            exitSplitScreen(mMainStage, EXIT_REASON_ROOT_TASK_VANISHED);
         }
 
         for (int i = mListeners.size() - 1; i >= 0; --i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 29e4b5b..9fcf98b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -353,7 +353,7 @@
             boolean isSeamlessDisplayChange = false;
 
             if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
-                if (info.getType() == TRANSIT_CHANGE) {
+                if (info.getType() == TRANSIT_CHANGE || isOnlyTranslucent) {
                     final int anim = getRotationAnimationHint(change, info, mDisplayController);
                     isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
                     if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 1d456ae..3f19149 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -203,6 +203,12 @@
     /** Transition type to minimize a task. */
     public static final int TRANSIT_MINIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 20;
 
+    /** Transition to start the recents transition */
+    public static final int TRANSIT_START_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 21;
+
+    /** Transition to end the recents transition */
+    public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22;
+
     /** Transition type for desktop mode transitions. */
     public static final int TRANSIT_DESKTOP_MODE_TYPES =
             WindowManager.TRANSIT_FIRST_CUSTOM + 100;
@@ -1875,6 +1881,8 @@
             case TRANSIT_SPLIT_PASSTHROUGH -> "SPLIT_PASSTHROUGH";
             case TRANSIT_CLEANUP_PIP_EXIT -> "CLEANUP_PIP_EXIT";
             case TRANSIT_MINIMIZE -> "MINIMIZE";
+            case TRANSIT_START_RECENTS_TRANSITION -> "START_RECENTS_TRANSITION";
+            case TRANSIT_END_RECENTS_TRANSITION -> "END_RECENTS_TRANSITION";
             default -> "";
         };
         return typeStr + "(FIRST_CUSTOM+" + (transitType - TRANSIT_FIRST_CUSTOM) + ")";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 7265fb8..c9f2d2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,6 +26,7 @@
 
 import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -110,6 +111,7 @@
                     }
                     mMainExecutor.execute(() -> {
                         mExclusionRegion.set(systemGestureExclusion);
+                        onExclusionRegionChanged(displayId, mExclusionRegion);
                     });
                 }
             };
@@ -163,7 +165,7 @@
             boolean isFocusedGlobally) {
         final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
         if (decor != null) {
-            decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+            decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion);
         }
     }
 
@@ -199,9 +201,9 @@
 
         if (enableDisplayFocusInShellTransitions()) {
             // Pass the current global focus status to avoid updates outside of a ShellTransition.
-            decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
+            decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion);
         } else {
-            decoration.relayout(taskInfo, taskInfo.isFocused);
+            decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion);
         }
     }
 
@@ -240,7 +242,7 @@
         } else {
             decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                     false /* setTaskCropAndPosition */,
-                    mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                    mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
         }
     }
 
@@ -254,7 +256,7 @@
 
         decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                 false /* setTaskCropAndPosition */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
     }
 
     @Override
@@ -266,6 +268,15 @@
         decoration.close();
     }
 
+    private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) {
+        final int decorCount = mWindowDecorByTaskId.size();
+        for (int i = 0; i < decorCount; i++) {
+            final CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+            if (decoration.mTaskInfo.displayId != displayId) continue;
+            decoration.onExclusionRegionChanged(exclusionRegion);
+        }
+    }
+
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             return true;
@@ -333,7 +344,7 @@
         windowDecoration.setTaskDragResizer(taskPositioner);
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
     }
 
     private class CaptionTouchEventListener implements
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index c954673..982fda0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -36,6 +36,7 @@
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.graphics.drawable.GradientDrawable;
 import android.os.Handler;
 import android.util.Size;
@@ -174,7 +175,8 @@
     }
 
     @Override
-    void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+    void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
         // The crop and position of the task should only be set when a task is fluid resizing. In
         // all other cases, it is expected that the transition handler positions and crops the task
@@ -186,7 +188,7 @@
         // synced with the buffer transaction (that draws the View). Both will be shown on screen
         // at the same, whereas applying them independently causes flickering. See b/270202228.
         relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
-                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
     }
 
     @VisibleForTesting
@@ -198,7 +200,8 @@
             boolean isStatusBarVisible,
             boolean isKeyguardVisibleAndOccluded,
             InsetsState displayInsetsState,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region globalExclusionRegion) {
         relayoutParams.reset();
         relayoutParams.mRunningTaskInfo = taskInfo;
         relayoutParams.mLayoutResId = R.layout.caption_window_decor;
@@ -210,6 +213,7 @@
         relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop;
         relayoutParams.mIsCaptionVisible = taskInfo.isFreeform()
                 || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+        relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion);
 
         if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
             // If the app is requesting to customize the caption bar, allow input to fall
@@ -236,7 +240,8 @@
     void relayout(RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region globalExclusionRegion) {
         final boolean isFreeform =
                 taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
         final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue()
@@ -249,7 +254,8 @@
         updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
                 shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
                 mIsKeyguardVisibleAndOccluded,
-                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
+                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+                globalExclusionRegion);
 
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f2d8a78..a2d81a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -220,6 +220,7 @@
                     }
                     mMainExecutor.execute(() -> {
                         mExclusionRegion.set(systemGestureExclusion);
+                        onExclusionRegionChanged(displayId, mExclusionRegion);
                     });
                 }
             };
@@ -432,7 +433,7 @@
             boolean isFocusedGlobally) {
         final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
         if (decor != null) {
-            decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+            decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion);
         }
     }
 
@@ -465,15 +466,15 @@
         final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo;
 
         if (taskInfo.displayId != oldTaskInfo.displayId
-                && !Flags.enableHandleInputFix()) {
+                && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             removeTaskFromEventReceiver(oldTaskInfo.displayId);
             incrementEventReceiverTasks(taskInfo.displayId);
         }
         if (enableDisplayFocusInShellTransitions()) {
             // Pass the current global focus status to avoid updates outside of a ShellTransition.
-            decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
+            decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion);
         } else {
-            decoration.relayout(taskInfo, taskInfo.isFocused);
+            decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion);
         }
         mActivityOrientationChangeHandler.ifPresent(handler ->
                 handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
@@ -514,7 +515,8 @@
         } else {
             decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                     false /* shouldSetTaskPositionAndCrop */,
-                    mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                    mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                    mExclusionRegion);
         }
     }
 
@@ -528,7 +530,8 @@
 
         decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                 false /* shouldSetTaskPositionAndCrop */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                mExclusionRegion);
     }
 
     @Override
@@ -539,7 +542,7 @@
         decoration.close();
         final int displayId = taskInfo.displayId;
         if (mEventReceiversByDisplay.contains(displayId)
-                && !Flags.enableHandleInputFix()) {
+                && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             removeTaskFromEventReceiver(displayId);
         }
         // Remove the decoration from the cache last because WindowDecoration#close could still
@@ -548,6 +551,15 @@
         mWindowDecorByTaskId.remove(taskInfo.taskId);
     }
 
+    private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) {
+        final int decorCount = mWindowDecorByTaskId.size();
+        for (int i = 0; i < decorCount; i++) {
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+            if (decoration.mTaskInfo.displayId != displayId) continue;
+            decoration.onExclusionRegionChanged(exclusionRegion);
+        }
+    }
+
     private void openHandleMenu(int taskId) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo)
@@ -750,10 +762,13 @@
 
         /**
          * Whether to pilfer the next motion event to send cancellations to the windows below.
-         * Useful when the caption window is spy and the gesture should be handle by the system
+         * Useful when the caption window is spy and the gesture should be handled by the system
          * instead of by the app for their custom header content.
+         * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because
+         * a spy window is not used then.
          */
-        private boolean mShouldPilferCaptionEvents;
+        private boolean mIsCustomHeaderGesture;
+        private boolean mIsResizeGesture;
         private boolean mIsDragging;
         private boolean mTouchscreenInUse;
         private boolean mHasLongClicked;
@@ -867,7 +882,7 @@
                 // to offset position relative to caption as a whole.
                 int[] viewLocation = new int[2];
                 v.getLocationInWindow(viewLocation);
-                final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e,
+                mIsResizeGesture = decoration.shouldResizeListenerHandleEvent(e,
                         new Point(viewLocation[0], viewLocation[1]));
                 // The caption window may be a spy window when the caption background is
                 // transparent, which means events will fall through to the app window. Make
@@ -875,21 +890,23 @@
                 // customizable region and what the app reported as exclusion areas, because
                 // the drag-move or other caption gestures should take priority outside those
                 // regions.
-                mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion
-                        && downInExclusionRegion && isTransparentCaption) && !isResizeEvent;
+                mIsCustomHeaderGesture = downInCustomizableCaptionRegion
+                        && downInExclusionRegion && isTransparentCaption;
             }
-            if (!mShouldPilferCaptionEvents) {
-                // The event will be handled by a window below or pilfered by resize handler.
+            if (mIsCustomHeaderGesture || mIsResizeGesture) {
+                // The event will be handled by the custom window below or pilfered by resize
+                // handler.
                 return false;
             }
-            // Otherwise pilfer so that windows below receive cancellations for this gesture, and
-            // continue normal handling as a caption gesture.
-            if (mInputManager != null) {
+            if (mInputManager != null
+                    && !Flags.enableAccessibleCustomHeaders()) {
+                // Pilfer so that windows below receive cancellations for this gesture.
                 mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
             }
             if (isUpOrCancel) {
                 // Gesture is finished, reset state.
-                mShouldPilferCaptionEvents = false;
+                mIsCustomHeaderGesture = false;
+                mIsResizeGesture = false;
             }
             if (isAppHandle) {
                 return mHandleDragDetector.onMotionEvent(v, e);
@@ -1234,7 +1251,7 @@
         relevantDecor.updateHoverAndPressStatus(ev);
         final int action = ev.getActionMasked();
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
-            if (!mTransitionDragActive && !Flags.enableHandleInputFix()) {
+            if (!mTransitionDragActive && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
                 relevantDecor.closeHandleMenuIfNeeded(ev);
             }
         }
@@ -1277,7 +1294,7 @@
                 }
                 final boolean shouldStartTransitionDrag =
                         relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
-                                || Flags.enableHandleInputFix();
+                                || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue();
                 if (dragFromStatusBarAllowed && shouldStartTransitionDrag) {
                     mTransitionDragActive = true;
                 }
@@ -1592,8 +1609,9 @@
         windowDecoration.setDragPositioningCallback(taskPositioner);
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
-        if (!Flags.enableHandleInputFix()) {
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                mExclusionRegion);
+        if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             incrementEventReceiverTasks(taskInfo.displayId);
         }
     }
@@ -1618,6 +1636,7 @@
         pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive);
         pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay);
         pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
+        pw.println(innerPrefix + "mExclusionRegion=" + mExclusionRegion);
     }
 
     private class DesktopModeOnTaskRepositionAnimationListener
@@ -1754,7 +1773,7 @@
                         && Flags.enableDesktopWindowingImmersiveHandleHiding()) {
                     decor.onInsetsStateChanged(insetsState);
                 }
-                if (!Flags.enableHandleInputFix()) {
+                if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
                     // If status bar inset is visible, top task is not in immersive mode.
                     // This value is only needed when the App Handle input is being handled
                     // through the global input monitor (hence the flag check) to ignore gestures
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index d97632a..cdcf14e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -394,7 +394,8 @@
     }
 
     @Override
-    void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+    void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
         // The visibility, crop and position of the task should only be set when a task is
         // fluid resizing. In all other cases, it is expected that the transition handler sets
@@ -415,7 +416,7 @@
         // causes flickering. See b/270202228.
         final boolean applyTransactionOnDraw = taskInfo.isFreeform();
         relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
-                hasGlobalFocus);
+                hasGlobalFocus, displayExclusionRegion);
         if (!applyTransactionOnDraw) {
             t.apply();
         }
@@ -442,18 +443,18 @@
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         Trace.beginSection("DesktopModeWindowDecoration#relayout");
         if (taskInfo.isFreeform()) {
             // The Task is in Freeform mode -> show its header in sync since it's an integral part
             // of the window itself - a delayed header might cause bad UX.
             relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         } else {
             // The Task is outside Freeform mode -> allow the handle view to be delayed since the
             // handle is just a small addition to the window.
             relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         }
         Trace.endSection();
     }
@@ -462,11 +463,11 @@
     private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         // Clear the current ViewHost runnable as we will update the ViewHost here
         clearCurrentViewHostRunnable();
         updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         if (mResult.mRootView != null) {
             updateViewHost(mRelayoutParams, startT, mResult);
         }
@@ -489,7 +490,8 @@
     private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         if (applyStartTransactionOnDraw) {
             throw new IllegalArgumentException(
                     "We cannot both sync viewhost ondraw and delay viewhost creation.");
@@ -498,7 +500,7 @@
         clearCurrentViewHostRunnable();
         updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop,
-                hasGlobalFocus);
+                hasGlobalFocus, displayExclusionRegion);
         if (mResult.mRootView == null) {
             // This means something blocks the window decor from showing, e.g. the task is hidden.
             // Nothing is set up in this case including the decoration surface.
@@ -513,7 +515,7 @@
     private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
         if (Flags.enableDesktopWindowingAppToWeb()) {
             setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp);
@@ -538,7 +540,8 @@
         updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
                 shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
                 mIsKeyguardVisibleAndOccluded, inFullImmersive,
-                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
+                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+                displayExclusionRegion);
 
         final WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -628,13 +631,6 @@
 
     @Nullable
     private Intent getBrowserLink() {
-        // Do not show browser link in browser applications
-        final ComponentName baseActivity = mTaskInfo.baseActivity;
-        if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
-                baseActivity.getPackageName(), mUserContext.getUserId())) {
-            return null;
-        }
-
         final Uri browserLink;
         // If the captured link is available and has not expired, return the captured link.
         // Otherwise, return the generic link which is set to null if a generic link is unavailable.
@@ -651,6 +647,18 @@
 
     }
 
+    @Nullable
+    private Intent getAppLink() {
+        return mWebUri == null ? null
+                : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager());
+    }
+
+    private boolean isBrowserApp() {
+        final ComponentName baseActivity = mTaskInfo.baseActivity;
+        return baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
+                baseActivity.getPackageName(), mUserContext.getUserId());
+    }
+
     UserHandle getUser() {
         return mUserContext.getUser();
     }
@@ -807,7 +815,7 @@
      */
     void disposeStatusBarInputLayer() {
         if (!isAppHandle(mWindowDecorViewHolder)
-                || !Flags.enableHandleInputFix()) {
+                || !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             return;
         }
         asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer();
@@ -874,7 +882,8 @@
             boolean isKeyguardVisibleAndOccluded,
             boolean inFullImmersiveMode,
             @NonNull InsetsState displayInsetsState,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
         final boolean isAppHeader =
                 captionLayoutId == R.layout.desktop_mode_app_header;
@@ -885,6 +894,7 @@
         relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
         relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
         relayoutParams.mHasGlobalFocus = hasGlobalFocus;
+        relayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion);
 
         final boolean showCaption;
         if (Flags.enableFullyImmersiveInDesktop()) {
@@ -910,10 +920,20 @@
         relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode;
         if (isAppHeader) {
             if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
-                // If the app is requesting to customize the caption bar, allow input to fall
-                // through to the windows below so that the app can respond to input events on
-                // their custom content.
-                relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+                // The app is requesting to customize the caption bar, which means input on
+                // customizable/exclusion regions must go to the app instead of to the system.
+                // This may be accomplished with spy windows or custom touchable regions:
+                if (Flags.enableAccessibleCustomHeaders()) {
+                    // Set the touchable region of the caption to only the areas where input should
+                    // be handled by the system (i.e. non custom-excluded areas). The region will
+                    // be calculated based on occluding caption elements and exclusion areas
+                    // reported by the app.
+                    relayoutParams.mLimitTouchRegionToSystemAreas = true;
+                } else {
+                    // Allow input to fall through to the windows below so that the app can respond
+                    // to input events on their custom content.
+                    relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+                }
             } else {
                 if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) {
                     // Force-consume the caption bar insets when the app tries to hide the caption.
@@ -951,7 +971,7 @@
             }
             controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END;
             relayoutParams.mOccludingCaptionElements.add(controlsElement);
-        } else if (isAppHandle && !Flags.enableHandleInputFix()) {
+        } else if (isAppHandle && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             // The focused decor (fullscreen/split) does not need to handle input because input in
             // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel.
             // Note: This does not apply with the above flag enabled as the status bar input layer
@@ -1368,6 +1388,7 @@
                 .shouldShowChangeAspectRatioButton(mTaskInfo);
         final boolean inDesktopImmersive = mDesktopRepository
                 .isTaskInFullImmersiveState(mTaskInfo.taskId);
+        final boolean isBrowserApp = isBrowserApp();
         mHandleMenu = mHandleMenuFactory.create(
                 this,
                 mWindowManagerWrapper,
@@ -1379,7 +1400,8 @@
                 supportsMultiInstance,
                 shouldShowManageWindowsButton,
                 shouldShowChangeAspectRatioButton,
-                getBrowserLink(),
+                isBrowserApp,
+                isBrowserApp ? getAppLink() : getBrowserLink(),
                 mResult.mCaptionWidth,
                 mResult.mCaptionHeight,
                 mResult.mCaptionX,
@@ -1560,13 +1582,13 @@
      */
     boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
         if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder)
-                || Flags.enableHandleInputFix()) {
+                || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             return false;
         }
         // The status bar input layer can only receive input in handle coordinates to begin with,
         // so checking coordinates is unnecessary as input is always within handle bounds.
         if (isAppHandle(mWindowDecorViewHolder)
-                && Flags.enableHandleInputFix()
+                && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()
                 && isCaptionVisible()) {
             return true;
         }
@@ -1603,7 +1625,7 @@
      * @param ev the MotionEvent to compare
      */
     void checkTouchEvent(MotionEvent ev) {
-        if (mResult.mRootView == null || Flags.enableHandleInputFix()) return;
+        if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return;
         final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
         final View handle = caption.findViewById(R.id.caption_handle);
         final boolean inHandle = !isHandleMenuActive()
@@ -1616,7 +1638,7 @@
             // If the whole handle menu can be touched directly, rely on FLAG_WATCH_OUTSIDE_TOUCH.
             // This is for the case that some of the handle menu is underneath the status bar.
             if (isAppHandle(mWindowDecorViewHolder)
-                    && !Flags.enableHandleInputFix()) {
+                    && !DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
                 mHandleMenu.checkMotionEvent(ev);
                 closeHandleMenuIfNeeded(ev);
             }
@@ -1630,7 +1652,7 @@
      * @param ev the MotionEvent to compare against.
      */
     void updateHoverAndPressStatus(MotionEvent ev) {
-        if (mResult.mRootView == null || Flags.enableHandleInputFix()) return;
+        if (mResult.mRootView == null || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return;
         final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
         final boolean inHandle = !isHandleMenuActive()
                 && checkTouchEventInFocusedCaptionHandle(ev);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 2edc380..54c247b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -39,12 +39,14 @@
 import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.TextView
+import android.window.DesktopModeFlags
 import android.window.SurfaceSyncGroup
+import androidx.annotation.StringRes
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.graphics.toArgb
 import androidx.core.view.isGone
-import com.android.window.flags.Flags
 import com.android.wm.shell.R
+import com.android.wm.shell.apptoweb.isBrowserApp
 import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -73,7 +75,8 @@
     private val shouldShowNewWindowButton: Boolean,
     private val shouldShowManageWindowsButton: Boolean,
     private val shouldShowChangeAspectRatioButton: Boolean,
-    private val openInBrowserIntent: Intent?,
+    private val isBrowserApp: Boolean,
+    private val openInAppOrBrowserIntent: Intent?,
     private val captionWidth: Int,
     private val captionHeight: Int,
     captionX: Int,
@@ -83,7 +86,7 @@
     private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo
 
     private val isViewAboveStatusBar: Boolean
-        get() = (Flags.enableHandleInputFix() && !taskInfo.isFreeform)
+        get() = (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() && !taskInfo.isFreeform)
 
     private val pillElevation: Int = loadDimensionPixelSize(
         R.dimen.desktop_mode_handle_menu_pill_elevation)
@@ -111,7 +114,7 @@
     private val globalMenuPosition: Point = Point()
 
     private val shouldShowBrowserPill: Boolean
-        get() = openInBrowserIntent != null
+        get() = openInAppOrBrowserIntent != null
 
     private val shouldShowMoreActionsPill: Boolean
         get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton ||
@@ -128,7 +131,7 @@
         onNewWindowClickListener: () -> Unit,
         onManageWindowsClickListener: () -> Unit,
         onChangeAspectRatioClickListener: () -> Unit,
-        openInBrowserClickListener: (Intent) -> Unit,
+        openInAppOrBrowserClickListener: (Intent) -> Unit,
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
         onOutsideTouchListener: () -> Unit,
@@ -146,7 +149,7 @@
             onNewWindowClickListener = onNewWindowClickListener,
             onManageWindowsClickListener = onManageWindowsClickListener,
             onChangeAspectRatioClickListener = onChangeAspectRatioClickListener,
-            openInBrowserClickListener = openInBrowserClickListener,
+            openInAppOrBrowserClickListener = openInAppOrBrowserClickListener,
             onOpenByDefaultClickListener = onOpenByDefaultClickListener,
             onCloseMenuClickListener = onCloseMenuClickListener,
             onOutsideTouchListener = onOutsideTouchListener,
@@ -167,7 +170,7 @@
         onNewWindowClickListener: () -> Unit,
         onManageWindowsClickListener: () -> Unit,
         onChangeAspectRatioClickListener: () -> Unit,
-        openInBrowserClickListener: (Intent) -> Unit,
+        openInAppOrBrowserClickListener: (Intent) -> Unit,
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
         onOutsideTouchListener: () -> Unit,
@@ -181,7 +184,8 @@
             shouldShowBrowserPill = shouldShowBrowserPill,
             shouldShowNewWindowButton = shouldShowNewWindowButton,
             shouldShowManageWindowsButton = shouldShowManageWindowsButton,
-            shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton
+            shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton,
+            isBrowserApp = isBrowserApp
         ).apply {
             bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill)
             this.onToDesktopClickListener = onToDesktopClickListener
@@ -190,8 +194,8 @@
             this.onNewWindowClickListener = onNewWindowClickListener
             this.onManageWindowsClickListener = onManageWindowsClickListener
             this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener
-            this.onOpenInBrowserClickListener = {
-                openInBrowserClickListener.invoke(openInBrowserIntent!!)
+            this.onOpenInAppOrBrowserClickListener = {
+                openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!)
             }
             this.onOpenByDefaultClickListener = onOpenByDefaultClickListener
             this.onCloseMenuClickListener = onCloseMenuClickListener
@@ -201,7 +205,8 @@
         val x = handleMenuPosition.x.toInt()
         val y = handleMenuPosition.y.toInt()
         handleMenuViewContainer =
-            if ((!taskInfo.isFreeform && Flags.enableHandleInputFix()) || forceShowSystemBars) {
+            if ((!taskInfo.isFreeform && DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue())
+                    || forceShowSystemBars) {
                 AdditionalSystemViewContainer(
                     windowManagerWrapper = windowManagerWrapper,
                     taskId = taskInfo.taskId,
@@ -237,7 +242,7 @@
             menuX = marginMenuStart
             menuY = captionY + marginMenuTop
         } else {
-            if (Flags.enableHandleInputFix()) {
+            if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
                 // In a focused decor, we use global coordinates for handle menu. Therefore we
                 // need to account for other factors like split stage and menu/handle width to
                 // center the menu.
@@ -435,14 +440,15 @@
     /** The view within the Handle Menu, with options to change the windowing mode and more. */
     @SuppressLint("ClickableViewAccessibility")
     class HandleMenuView(
-        context: Context,
+        private val context: Context,
         menuWidth: Int,
         captionHeight: Int,
         private val shouldShowWindowingPill: Boolean,
         private val shouldShowBrowserPill: Boolean,
         private val shouldShowNewWindowButton: Boolean,
         private val shouldShowManageWindowsButton: Boolean,
-        private val shouldShowChangeAspectRatioButton: Boolean
+        private val shouldShowChangeAspectRatioButton: Boolean,
+        private val isBrowserApp: Boolean
     ) {
         val rootView = LayoutInflater.from(context)
             .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
@@ -472,11 +478,12 @@
         private val changeAspectRatioBtn = moreActionsPill
             .requireViewById<Button>(R.id.change_aspect_ratio_button)
 
-        // Open in Browser Pill.
-        private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill)
-        private val browserBtn = openInBrowserPill.requireViewById<Button>(
-            R.id.open_in_browser_button)
-        private val openByDefaultBtn = openInBrowserPill.requireViewById<ImageButton>(
+        // Open in Browser/App Pill.
+        private val openInAppOrBrowserPill = rootView.requireViewById<View>(
+            R.id.open_in_app_or_browser_pill)
+        private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>(
+            R.id.open_in_app_or_browser_button)
+        private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>(
             R.id.open_by_default_button)
         private val decorThemeUtil = DecorThemeUtil(context)
         private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat())
@@ -490,7 +497,7 @@
         var onNewWindowClickListener: (() -> Unit)? = null
         var onManageWindowsClickListener: (() -> Unit)? = null
         var onChangeAspectRatioClickListener: (() -> Unit)? = null
-        var onOpenInBrowserClickListener: (() -> Unit)? = null
+        var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null
         var onOpenByDefaultClickListener: (() -> Unit)? = null
         var onCloseMenuClickListener: (() -> Unit)? = null
         var onOutsideTouchListener: (() -> Unit)? = null
@@ -499,7 +506,7 @@
             fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() }
             splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() }
             desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() }
-            browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() }
+            openInAppOrBrowserBtn.setOnClickListener { onOpenInAppOrBrowserClickListener?.invoke() }
             openByDefaultBtn.setOnClickListener {
                 onOpenByDefaultClickListener?.invoke()
             }
@@ -535,10 +542,10 @@
             if (shouldShowMoreActionsPill) {
                 bindMoreActionsPill(style)
             }
-            bindOpenInBrowserPill(style)
+            bindOpenInAppOrBrowserPill(style)
         }
 
-        /** Animates the menu opening. */
+        /** Animates the menu openInAppOrBrowserg. */
         fun animateOpenMenu() {
             if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
                 animator.animateCaptionHandleExpandToOpen()
@@ -660,13 +667,20 @@
             }
         }
 
-        private fun bindOpenInBrowserPill(style: MenuStyle) {
-            openInBrowserPill.apply {
+        private fun bindOpenInAppOrBrowserPill(style: MenuStyle) {
+            openInAppOrBrowserPill.apply {
                 isGone = !shouldShowBrowserPill
                 background.setTint(style.backgroundColor)
             }
 
-            browserBtn.apply {
+            val btnText = if (isBrowserApp) {
+                getString(R.string.open_in_app_text)
+            } else {
+                getString(R.string.open_in_browser_text)
+            }
+            openInAppOrBrowserBtn.apply {
+                text = btnText
+                contentDescription = btnText
                 setTextColor(style.textColor)
                 compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
             }
@@ -674,6 +688,8 @@
             openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
         }
 
+        private fun getString(@StringRes resId: Int): String = context.resources.getString(resId)
+
         private data class MenuStyle(
             @ColorInt val backgroundColor: Int,
             @ColorInt val textColor: Int,
@@ -708,7 +724,8 @@
         shouldShowNewWindowButton: Boolean,
         shouldShowManageWindowsButton: Boolean,
         shouldShowChangeAspectRatioButton: Boolean,
-        openInBrowserIntent: Intent?,
+        isBrowserApp: Boolean,
+        openInAppOrBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
         captionX: Int,
@@ -729,7 +746,8 @@
         shouldShowNewWindowButton: Boolean,
         shouldShowManageWindowsButton: Boolean,
         shouldShowChangeAspectRatioButton: Boolean,
-        openInBrowserIntent: Intent?,
+        isBrowserApp: Boolean,
+        openInAppOrBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
         captionX: Int,
@@ -746,7 +764,8 @@
             shouldShowNewWindowButton,
             shouldShowManageWindowsButton,
             shouldShowChangeAspectRatioButton,
-            openInBrowserIntent,
+            isBrowserApp,
+            openInAppOrBrowserIntent,
             captionWidth,
             captionHeight,
             captionX,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 0c475f1..470e5a1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -74,7 +74,8 @@
     private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
     private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
     private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill)
-    private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill)
+    private val openInAppOrBrowserPill: ViewGroup =
+        handleMenu.requireViewById(R.id.open_in_app_or_browser_pill)
 
     /** Animates the opening of the handle menu. */
     fun animateOpen() {
@@ -83,7 +84,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
-        animateOpenInBrowserPill()
+        animateOpenInAppOrBrowserPill()
         runAnimations {
             appInfoPill.post {
                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
@@ -103,7 +104,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
-        animateOpenInBrowserPill()
+        animateOpenInAppOrBrowserPill()
         runAnimations {
             appInfoPill.post {
                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
@@ -124,7 +125,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
-        openInBrowserPillClose()
+        openInAppOrBrowserPillClose()
         runAnimations(after)
     }
 
@@ -141,7 +142,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
-        openInBrowserPillClose()
+        openInAppOrBrowserPillClose()
         runAnimations(after)
     }
 
@@ -154,7 +155,7 @@
         appInfoPill.children.forEach { it.alpha = 0f }
         windowingPill.alpha = 0f
         moreActionsPill.alpha = 0f
-        openInBrowserPill.alpha = 0f
+        openInAppOrBrowserPill.alpha = 0f
 
         // Setup pivots.
         handleMenu.pivotX = menuWidth / 2f
@@ -166,8 +167,8 @@
         moreActionsPill.pivotX = menuWidth / 2f
         moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
 
-        openInBrowserPill.pivotX = menuWidth / 2f
-        openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
+        openInAppOrBrowserPill.pivotX = menuWidth / 2f
+        openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
     }
 
     private fun animateAppInfoPillOpen() {
@@ -297,36 +298,36 @@
         }
     }
 
-    private fun animateOpenInBrowserPill() {
+    private fun animateOpenInAppOrBrowserPill() {
         // Open in Browser X & Y Scaling Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
                     startDelay = BODY_SCALE_OPEN_DELAY
                     duration = BODY_SCALE_OPEN_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
                     startDelay = BODY_SCALE_OPEN_DELAY
                     duration = BODY_SCALE_OPEN_DURATION
                 }
 
         // Open in Browser Opacity Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply {
                     startDelay = BODY_ALPHA_OPEN_DELAY
                     duration = BODY_ALPHA_OPEN_DURATION
                 }
 
         // Open in Browser Elevation Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply {
                     startDelay = ELEVATION_OPEN_DELAY
                     duration = BODY_ELEVATION_OPEN_DURATION
                 }
 
         // Open in Browser Button Opacity Animation
-        val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button)
+        val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button)
         animators +=
                 ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
                     startDelay = BODY_ALPHA_OPEN_DELAY
@@ -438,33 +439,33 @@
             }
     }
 
-    private fun openInBrowserPillClose() {
+    private fun openInAppOrBrowserPillClose() {
         // Open in Browser X & Y Scaling Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         // Open in Browser Opacity Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         // Upward Open in Browser y-translation Animation
         val yStart: Float = -captionHeight / 2
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply {
                     duration = BODY_CLOSE_DURATION
                 }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
index cf82bb4f9..8bc56e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
@@ -16,13 +16,12 @@
 package com.android.wm.shell.windowdecor
 
 import android.app.ActivityManager.RunningTaskInfo
-import com.android.window.flags.Flags
-import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
-
 import android.content.Context
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.widget.ImageButton
+import android.window.DesktopModeFlags
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
 
 /**
  * A custom [ImageButton] for buttons inside handle menu that intentionally doesn't handle hovers.
@@ -39,7 +38,7 @@
     lateinit var taskInfo: RunningTaskInfo
 
     override fun onHoverEvent(motionEvent: MotionEvent): Boolean {
-        if (Flags.enableHandleInputFix() || taskInfo.isFreeform) {
+        if (DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue() || taskInfo.isFreeform) {
             return super.onHoverEvent(motionEvent)
         } else {
             return false
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index b016c75..a3c75bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -127,7 +127,7 @@
                     }
 
                     mDisplayController.removeDisplayWindowListener(this);
-                    relayout(mTaskInfo, mHasGlobalFocus);
+                    relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
                 }
             };
 
@@ -143,7 +143,7 @@
     SurfaceControl mDecorationContainerSurface;
 
     SurfaceControl mCaptionContainerSurface;
-    private WindowlessWindowManager mCaptionWindowManager;
+    private CaptionWindowlessWindowManager mCaptionWindowManager;
     private SurfaceControlViewHost mViewHost;
     private Configuration mWindowDecorConfig;
     TaskDragResizer mTaskDragResizer;
@@ -152,6 +152,7 @@
     boolean mIsStatusBarVisible;
     boolean mIsKeyguardVisibleAndOccluded;
     boolean mHasGlobalFocus;
+    final Region mExclusionRegion = Region.obtain();
 
     /** The most recent set of insets applied to this window decoration. */
     private WindowDecorationInsets mWindowDecorationInsets;
@@ -218,7 +219,8 @@
      *                 constructor.
      * @param hasGlobalFocus Whether the task is focused
      */
-    abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus);
+    abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion);
 
     /**
      * Used by the {@link DragPositioningCallback} associated with the implementing class to
@@ -244,6 +246,7 @@
             mTaskInfo = params.mRunningTaskInfo;
         }
         mHasGlobalFocus = params.mHasGlobalFocus;
+        mExclusionRegion.set(params.mDisplayExclusionRegion);
         final int oldLayoutResId = mLayoutResId;
         mLayoutResId = params.mLayoutResId;
 
@@ -402,7 +405,7 @@
                 final int elementWidthPx =
                         resources.getDimensionPixelSize(element.mWidthResId);
                 boundingRects[i] =
-                        calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
+                        calculateBoundingRectLocal(element, elementWidthPx, captionInsetsRect);
                 // Subtract the regions used by the caption elements, the rest is
                 // customizable.
                 if (params.hasInputFeatureSpy()) {
@@ -477,9 +480,8 @@
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
             // of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
-            mCaptionWindowManager = new WindowlessWindowManager(
-                    mTaskInfo.getConfiguration(), mCaptionContainerSurface,
-                    null /* hostInputToken */);
+            mCaptionWindowManager = new CaptionWindowlessWindowManager(
+                    mTaskInfo.getConfiguration(), mCaptionContainerSurface);
         }
         mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration());
         final WindowManager.LayoutParams lp =
@@ -492,6 +494,14 @@
         lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
         lp.setTrustedOverlay();
         lp.inputFeatures = params.mInputFeatures;
+        final Rect localCaptionBounds = new Rect(
+                outResult.mCaptionX,
+                outResult.mCaptionY,
+                outResult.mCaptionX + outResult.mCaptionWidth,
+                outResult.mCaptionY + outResult.mCaptionHeight);
+        final Region touchableRegion = params.mLimitTouchRegionToSystemAreas
+                ? calculateLimitedTouchableRegion(params, localCaptionBounds)
+                : null;
         if (mViewHost == null) {
             Trace.beginSection("CaptionViewHostLayout-new");
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
@@ -503,6 +513,9 @@
                 mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0);
+            if (params.mLimitTouchRegionToSystemAreas) {
+                mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion);
+            }
             mViewHost.setView(outResult.mRootView, lp);
             Trace.endSection();
         } else {
@@ -514,13 +527,71 @@
                 mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0);
+            if (params.mLimitTouchRegionToSystemAreas) {
+                mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion);
+            }
             mViewHost.relayout(lp);
             Trace.endSection();
         }
+        if (touchableRegion != null) {
+            touchableRegion.recycle();
+        }
         Trace.endSection(); // CaptionViewHostLayout
     }
 
-    private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element,
+    @NonNull
+    private Region calculateLimitedTouchableRegion(
+            RelayoutParams params,
+            @NonNull Rect localCaptionBounds) {
+        // Make caption bounds relative to display to align with exclusion region.
+        final Point positionInParent = params.mRunningTaskInfo.positionInParent;
+        final Rect captionBoundsInDisplay = new Rect(localCaptionBounds);
+        captionBoundsInDisplay.offsetTo(positionInParent.x, positionInParent.y);
+
+        final Region boundingRects = calculateBoundingRectsRegion(params, captionBoundsInDisplay);
+
+        final Region customizedRegion = Region.obtain();
+        customizedRegion.set(captionBoundsInDisplay);
+        customizedRegion.op(boundingRects, Region.Op.DIFFERENCE);
+        customizedRegion.op(params.mDisplayExclusionRegion, Region.Op.INTERSECT);
+
+        final Region touchableRegion = Region.obtain();
+        touchableRegion.set(captionBoundsInDisplay);
+        touchableRegion.op(customizedRegion, Region.Op.DIFFERENCE);
+        // Return resulting region back to window coordinates.
+        touchableRegion.translate(-positionInParent.x, -positionInParent.y);
+
+        boundingRects.recycle();
+        customizedRegion.recycle();
+        return touchableRegion;
+    }
+
+    @NonNull
+    private Region calculateBoundingRectsRegion(
+            @NonNull RelayoutParams params,
+            @NonNull Rect captionBoundsInDisplay) {
+        final int numOfElements = params.mOccludingCaptionElements.size();
+        final Region region = Region.obtain();
+        if (numOfElements == 0) {
+            // The entire caption is a bounding rect.
+            region.set(captionBoundsInDisplay);
+            return region;
+        }
+        final Resources resources = mDecorWindowContext.getResources();
+        for (int i = 0; i < numOfElements; i++) {
+            final OccludingCaptionElement element = params.mOccludingCaptionElements.get(i);
+            final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId);
+            final Rect boundingRect = calculateBoundingRectLocal(element, elementWidthPx,
+                    captionBoundsInDisplay);
+            // Bounding rect is initially calculated relative to the caption, so offset it to make
+            // it relative to the display.
+            boundingRect.offset(captionBoundsInDisplay.left, captionBoundsInDisplay.top);
+            region.union(boundingRect);
+        }
+        return region;
+    }
+
+    private Rect calculateBoundingRectLocal(@NonNull OccludingCaptionElement element,
             int elementWidthPx, @NonNull Rect captionRect) {
         switch (element.mAlignment) {
             case START -> {
@@ -539,7 +610,7 @@
         mIsKeyguardVisibleAndOccluded = visible && occluded;
         final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded;
         if (changed) {
-            relayout(mTaskInfo, mHasGlobalFocus);
+            relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
         }
     }
 
@@ -549,10 +620,14 @@
         final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible;
 
         if (changed) {
-            relayout(mTaskInfo, mHasGlobalFocus);
+            relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
         }
     }
 
+    void onExclusionRegionChanged(@NonNull Region exclusionRegion) {
+        relayout(mTaskInfo, mHasGlobalFocus, exclusionRegion);
+    }
+
     /**
      * Update caption visibility state and views.
      */
@@ -751,9 +826,11 @@
         int mCaptionHeightId;
         int mCaptionWidthId;
         final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>();
+        boolean mLimitTouchRegionToSystemAreas;
         int mInputFeatures;
         boolean mIsInsetSource = true;
         @InsetsSource.Flags int mInsetSourceFlags;
+        final Region mDisplayExclusionRegion = Region.obtain();
 
         int mShadowRadiusId;
         int mCornerRadius;
@@ -772,9 +849,11 @@
             mCaptionHeightId = Resources.ID_NULL;
             mCaptionWidthId = Resources.ID_NULL;
             mOccludingCaptionElements.clear();
+            mLimitTouchRegionToSystemAreas = false;
             mInputFeatures = 0;
             mIsInsetSource = true;
             mInsetSourceFlags = 0;
+            mDisplayExclusionRegion.setEmpty();
 
             mShadowRadiusId = Resources.ID_NULL;
             mCornerRadius = 0;
@@ -830,6 +909,19 @@
         }
     }
 
+    private static class CaptionWindowlessWindowManager extends WindowlessWindowManager {
+        CaptionWindowlessWindowManager(
+                @NonNull Configuration configuration,
+                @NonNull SurfaceControl rootSurface) {
+            super(configuration, rootSurface, /* hostInputToken= */ null);
+        }
+
+        /** Set the view host's touchable region. */
+        void setTouchRegion(@NonNull SurfaceControlViewHost viewHost, @NonNull Region region) {
+            setTouchRegion(viewHost.getWindowToken().asBinder(), region);
+        }
+    }
+
     @VisibleForTesting
     public interface SurfaceControlViewHostFactory {
         default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index b5700ff..503ad92 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -34,15 +34,14 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
 import android.widget.ImageButton
+import android.window.DesktopModeFlags
 import androidx.core.view.ViewCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
 import com.android.internal.policy.SystemBarUtils
-import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.shared.animation.Interpolators
 import com.android.wm.shell.windowdecor.WindowManagerWrapper
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
-import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
 
 /**
  * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
@@ -141,7 +140,7 @@
     private fun createStatusBarInputLayer(handlePosition: Point,
                                           handleWidth: Int,
                                           handleHeight: Int) {
-        if (!Flags.enableHandleInputFix()) return
+        if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) return
         statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper,
             taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
             WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index 4fe66f3..4cddf31 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -23,8 +23,9 @@
 import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
 import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
 import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
-import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible
 import android.tools.flicker.assertors.assertions.AppWindowAlignsWithOnlyOneDisplayCornerAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowBecomesInvisible
+import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible
 import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd
 import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd
 import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
@@ -44,6 +45,7 @@
 import android.tools.flicker.config.AssertionTemplates
 import android.tools.flicker.config.FlickerConfigEntry
 import android.tools.flicker.config.ScenarioId
+import android.tools.flicker.config.common.Components.LAUNCHER
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER
 import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP
@@ -365,5 +367,57 @@
                             AppWindowAlignsWithOnlyOneDisplayCornerAtEnd(DESKTOP_MODE_APP)
                         ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
             )
+
+        val MINIMIZE_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("MINIMIZE_APP"),
+                extractor =
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions
+                                .filter { it.type == TransitionType.MINIMIZE }
+                                .sortedByDescending { it.id }
+                                .drop(1)
+                        }
+                    }
+                ),
+                assertions =
+                AssertionTemplates.COMMON_ASSERTIONS +
+                    listOf(
+                        AppWindowOnTopAtStart(DESKTOP_MODE_APP),
+                        AppWindowBecomesInvisible(DESKTOP_MODE_APP),
+                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
+            )
+
+        val MINIMIZE_LAST_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("MINIMIZE_LAST_APP"),
+                extractor =
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            val lastTransition =
+                                transitions
+                                .filter { it.type == TransitionType.MINIMIZE }
+                                .maxByOrNull { it.id }!!
+                            return listOf(lastTransition)
+                        }
+                    }
+                ),
+                assertions =
+                AssertionTemplates.COMMON_ASSERTIONS +
+                    listOf(
+                        AppWindowOnTopAtStart(DESKTOP_MODE_APP),
+                        AppWindowBecomesInvisible(DESKTOP_MODE_APP),
+                        AppWindowOnTopAtEnd(LAUNCHER),
+                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
+            )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt
new file mode 100644
index 0000000..58582b0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP
+import com.android.wm.shell.scenarios.MinimizeAppWindows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Minimize app windows by pressing the minimize button.
+ *
+ * Assert that the app windows gets hidden.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MinimizeAppsLandscape : MinimizeAppWindows(rotation = ROTATION_90) {
+    @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"])
+    @Test
+    override fun minimizeAllAppWindows() = super.minimizeAllAppWindows()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(MINIMIZE_APP)
+                .use(MINIMIZE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt
new file mode 100644
index 0000000..7970426
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP
+import com.android.wm.shell.scenarios.MinimizeAppWindows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Minimize app windows by pressing the minimize button.
+ *
+ * Assert that the app windows gets hidden.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MinimizeAppsPortrait : MinimizeAppWindows() {
+    @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"])
+    @Test
+    override fun minimizeAllAppWindows() = super.minimizeAllAppWindows()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(MINIMIZE_APP)
+                .use(MINIMIZE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 824c448..f442fdb 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,6 +18,7 @@
 
 import android.tools.NavBar
 import android.tools.Rotation
+import com.android.internal.R
 import com.android.window.flags.Flags
 import com.android.wm.shell.Utils
 import org.junit.After
@@ -40,6 +41,9 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        // Skip the test when the drag-to-maximize is enabled on this device.
+        Assume.assumeFalse(Flags.enableDragToMaximize() &&
+            instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         testApp.enterDesktopWithDrag(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
index d38b848..329a109 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
@@ -16,9 +16,8 @@
 package com.android.wm.shell.bubbles.bar;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
-import android.graphics.drawable.ColorDrawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
@@ -47,10 +46,9 @@
     public void testUpdateHandleColor_lightBg() {
         mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */);
 
-        assertTrue(mHandleView.getClipToOutline());
-        assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
-        ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
-        assertEquals(bgDrawable.getColor(),
+        assertFalse(mHandleView.getClipToOutline());
+        int handleColor = mHandleView.mHandlePaint.getColor();
+        assertEquals(handleColor,
                 ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark));
     }
 
@@ -58,10 +56,9 @@
     public void testUpdateHandleColor_darkBg() {
         mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */);
 
-        assertTrue(mHandleView.getClipToOutline());
-        assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
-        ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
-        assertEquals(bgDrawable.getColor(),
+        assertFalse(mHandleView.getClipToOutline());
+        int handleColor = mHandleView.mHandlePaint.getColor();
+        assertEquals(handleColor,
                 ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light));
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java
new file mode 100644
index 0000000..b9490b8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.pip;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.common.ShellExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit test against {@link PipAppOpsListener}.
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class PipAppOpsListenerTest {
+
+    @Mock private Context mMockContext;
+    @Mock private PackageManager mMockPackageManager;
+    @Mock private AppOpsManager mMockAppOpsManager;
+    @Mock private PipAppOpsListener.Callback mMockCallback;
+    @Mock private ShellExecutor mMockExecutor;
+
+    private PipAppOpsListener mPipAppOpsListener;
+
+    private ArgumentCaptor<AppOpsManager.OnOpChangedListener> mOnOpChangedListenerCaptor;
+    private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
+    private Pair<ComponentName, Integer> mTopPipActivity;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockContext.getSystemService(Context.APP_OPS_SERVICE))
+                .thenReturn(mMockAppOpsManager);
+        mOnOpChangedListenerCaptor = ArgumentCaptor.forClass(
+                AppOpsManager.OnOpChangedListener.class);
+        mRunnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+    }
+
+    @Test
+    public void onActivityPinned_registerAppOpsListener() {
+        String packageName = "com.android.test.pip";
+        mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor);
+
+        mPipAppOpsListener.onActivityPinned(packageName);
+
+        verify(mMockAppOpsManager).startWatchingMode(
+                eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName),
+                any(AppOpsManager.OnOpChangedListener.class));
+    }
+
+    @Test
+    public void onActivityUnpinned_unregisterAppOpsListener() {
+        mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor);
+
+        mPipAppOpsListener.onActivityUnpinned();
+
+        verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class));
+    }
+
+    @Test
+    public void disablePipAppOps_dismissPip() throws PackageManager.NameNotFoundException {
+        String packageName = "com.android.test.pip";
+        mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor);
+        // Set up the top pip activity info as mTopPipActivity
+        mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0);
+        mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity);
+        // Set up the application info as mApplicationInfo
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.packageName = packageName;
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(applicationInfo);
+        // Mock the mode to be **not** allowed
+        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName)))
+                .thenReturn(AppOpsManager.MODE_DEFAULT);
+        // Set up the initial state
+        mPipAppOpsListener.onActivityPinned(packageName);
+        verify(mMockAppOpsManager).startWatchingMode(
+                eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName),
+                mOnOpChangedListenerCaptor.capture());
+        AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue();
+
+        opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE),
+                packageName);
+
+        verify(mMockExecutor).execute(mRunnableArgumentCaptor.capture());
+        Runnable runnable = mRunnableArgumentCaptor.getValue();
+        runnable.run();
+        verify(mMockCallback).dismissPip();
+    }
+
+    @Test
+    public void disablePipAppOps_differentPackage_doNothing()
+            throws PackageManager.NameNotFoundException {
+        String packageName = "com.android.test.pip";
+        mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor);
+        // Set up the top pip activity info as mTopPipActivity
+        mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0);
+        mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity);
+        // Set up the application info as mApplicationInfo
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.packageName = packageName + ".modified";
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(applicationInfo);
+        // Mock the mode to be **not** allowed
+        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName)))
+                .thenReturn(AppOpsManager.MODE_DEFAULT);
+        // Set up the initial state
+        mPipAppOpsListener.onActivityPinned(packageName);
+        verify(mMockAppOpsManager).startWatchingMode(
+                eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName),
+                mOnOpChangedListenerCaptor.capture());
+        AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue();
+
+        opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE),
+                packageName);
+
+        verifyZeroInteractions(mMockExecutor);
+    }
+
+    @Test
+    public void disablePipAppOps_nameNotFound_unregisterAppOpsListener()
+            throws PackageManager.NameNotFoundException {
+        String packageName = "com.android.test.pip";
+        mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor);
+        // Set up the top pip activity info as mTopPipActivity
+        mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0);
+        mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity);
+        // Set up the application info as mApplicationInfo
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.packageName = packageName;
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenThrow(PackageManager.NameNotFoundException.class);
+        // Mock the mode to be **not** allowed
+        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName)))
+                .thenReturn(AppOpsManager.MODE_DEFAULT);
+        // Set up the initial state
+        mPipAppOpsListener.onActivityPinned(packageName);
+        verify(mMockAppOpsManager).startWatchingMode(
+                eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName),
+                mOnOpChangedListenerCaptor.capture());
+        AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue();
+
+        opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE),
+                packageName);
+
+        verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class));
+    }
+
+    private Pair<ComponentName, Integer> getTopPipActivity(Context context) {
+        return mTopPipActivity;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
new file mode 100644
index 0000000..6df8d6f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.window.TransitionInfo
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() {
+
+    private val testExecutor = mock<ShellExecutor>()
+    private val closingTaskLeash = mock<SurfaceControl>()
+    private val displayController = mock<DisplayController>()
+
+    private lateinit var handler: DesktopBackNavigationTransitionHandler
+
+    @Before
+    fun setUp() {
+        handler =
+            DesktopBackNavigationTransitionHandler(
+                testExecutor,
+                testExecutor,
+                displayController
+            )
+        whenever(displayController.getDisplayContext(any())).thenReturn(mContext)
+    }
+
+    @Test
+    fun handleRequest_returnsNull() {
+        assertNull(handler.handleRequest(mock(), mock()))
+    }
+
+    @Test
+    fun startAnimation_openTransition_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info =
+                createTransitionInfo(
+                    type = WindowManager.TRANSIT_OPEN,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate open transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionFullscreenTask_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate fullscreen task to back transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionOpeningFreeformTask_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info =
+                createTransitionInfo(
+                    changeMode = WindowManager.TRANSIT_OPEN,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate opening freeform task to back transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionToBackFreeformTask_returnsTrue() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertTrue("Should animate going to back freeform task close transition", animates)
+    }
+
+    @Test
+    fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(
+                    type = TRANSIT_CLOSE,
+                    changeMode = TRANSIT_CLOSE,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertTrue("Should animate going to back freeform task close transition", animates)
+    }
+    private fun createTransitionInfo(
+        type: Int = WindowManager.TRANSIT_TO_BACK,
+        changeMode: Int = WindowManager.TRANSIT_TO_BACK,
+        task: RunningTaskInfo
+    ): TransitionInfo =
+        TransitionInfo(type, 0 /* flags */).apply {
+            addChange(
+                TransitionInfo.Change(mock(), closingTaskLeash).apply {
+                    mode = changeMode
+                    parent = null
+                    taskInfo = task
+                }
+            )
+        }
+
+    private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+        TestRunningTaskInfoBuilder()
+            .setActivityType(ACTIVITY_TYPE_STANDARD)
+            .setWindowingMode(windowingMode)
+            .build()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index e05a0b5..a4f4d05 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.desktopmode
 
+import android.animation.AnimatorTestRule
 import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
 import android.graphics.Rect
@@ -24,6 +25,7 @@
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.Surface
 import android.view.SurfaceControl
@@ -43,6 +45,7 @@
 import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.StubTransaction
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -64,17 +67,19 @@
  * Usage: atest WMShellUnitTests:DesktopImmersiveControllerTest
  */
 @SmallTest
+@TestableLooper.RunWithLooper
 @RunWith(AndroidTestingRunner::class)
 class DesktopImmersiveControllerTest : ShellTestCase() {
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
+    @JvmField @Rule val animatorTestRule = AnimatorTestRule(this)
 
     @Mock private lateinit var mockTransitions: Transitions
     private lateinit var desktopRepository: DesktopRepository
     @Mock private lateinit var mockDisplayController: DisplayController
     @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
     @Mock private lateinit var mockDisplayLayout: DisplayLayout
-    private val transactionSupplier = { SurfaceControl.Transaction() }
+    private val transactionSupplier = { StubTransaction() }
 
     private lateinit var controller: DesktopImmersiveController
 
@@ -89,10 +94,12 @@
             (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS)
         }
         controller = DesktopImmersiveController(
+            shellInit = mock(),
             transitions = mockTransitions,
             desktopRepository = desktopRepository,
             displayController = mockDisplayController,
             shellTaskOrganizer = mockShellTaskOrganizer,
+            shellCommandHandler = mock(),
             transactionSupplier = transactionSupplier,
         )
     }
@@ -672,6 +679,60 @@
         assertThat(controller.isImmersiveChange(transition, change)).isTrue()
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun externalAnimateResizeChange_doesNotCleanUpPendingTransitionState() {
+        val task = createFreeformTask()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
+            .thenReturn(mockBinder)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        controller.moveTaskToNonImmersive(task)
+
+        controller.animateResizeChange(
+            change = TransitionInfo.Change(task.token, SurfaceControl()).apply {
+                taskInfo = task
+            },
+            startTransaction = StubTransaction(),
+            finishTransaction = StubTransaction(),
+            finishCallback = { }
+        )
+        animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS)
+
+        assertThat(controller.state).isNotNull()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startAnimation_missingChange_clearsState() {
+        val task = createFreeformTask()
+        val mockBinder = mock(IBinder::class.java)
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
+            .thenReturn(mockBinder)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = task.displayId,
+            taskId = task.taskId,
+            immersive = false
+        )
+
+        controller.moveTaskToImmersive(task)
+
+        controller.startAnimation(
+            transition = mockBinder,
+            info = createTransitionInfo(changes = emptyList()),
+            startTransaction = StubTransaction(),
+            finishTransaction = StubTransaction(),
+            finishCallback = {}
+        )
+
+        assertThat(controller.state).isNull()
+    }
+
     private fun createTransitionInfo(
         @TransitionType type: Int = TRANSIT_CHANGE,
         @TransitionFlags flags: Int = 0,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index b06c2da..f21f264 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -32,6 +32,7 @@
 import android.view.SurfaceControl
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TransitionType
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
@@ -77,16 +78,28 @@
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-    @Mock lateinit var transitions: Transitions
-    @Mock lateinit var desktopRepository: DesktopRepository
-    @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
-    @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
-    @Mock lateinit var desktopImmersiveController: DesktopImmersiveController
-    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock lateinit var mockHandler: Handler
-    @Mock lateinit var closingTaskLeash: SurfaceControl
-    @Mock lateinit var shellInit: ShellInit
-    @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock
+    lateinit var transitions: Transitions
+    @Mock
+    lateinit var desktopRepository: DesktopRepository
+    @Mock
+    lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
+    @Mock
+    lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+    @Mock
+    lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler
+    @Mock
+    lateinit var desktopImmersiveController: DesktopImmersiveController
+    @Mock
+    lateinit var interactionJankMonitor: InteractionJankMonitor
+    @Mock
+    lateinit var mockHandler: Handler
+    @Mock
+    lateinit var closingTaskLeash: SurfaceControl
+    @Mock
+    lateinit var shellInit: ShellInit
+    @Mock
+    lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
 
     private lateinit var mixedHandler: DesktopMixedTransitionHandler
 
@@ -100,6 +113,7 @@
                 freeformTaskTransitionHandler,
                 closeDesktopTaskTransitionHandler,
                 desktopImmersiveController,
+                desktopBackNavigationTransitionHandler,
                 interactionJankMonitor,
                 mockHandler,
                 shellInit,
@@ -595,6 +609,87 @@
         assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun startAnimation_withMinimizingDesktopTask_callsBackNavigationHandler() {
+        val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
+        whenever(
+            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+        )
+            .thenReturn(true)
+        mixedHandler.addPendingMixedTransition(
+            PendingMixedTransition.Minimize(
+                transition = transition,
+                minimizingTask = minimizingTask.taskId,
+                isLastTask = false,
+            )
+        )
+
+        val minimizingTaskChange = createChange(minimizingTask)
+        val started = mixedHandler.startAnimation(
+            transition = transition,
+            info =
+                createTransitionInfo(
+                TRANSIT_TO_BACK,
+                listOf(minimizingTaskChange)
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        assertTrue("Should delegate animation to back navigation transition handler", started)
+        verify(desktopBackNavigationTransitionHandler)
+            .startAnimation(
+                eq(transition),
+                argThat { info -> info.changes.contains(minimizingTaskChange) },
+                any(), any(), any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun startAnimation_withMinimizingLastDesktopTask_dispatchesTransition() {
+        val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
+        whenever(
+            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+        )
+            .thenReturn(true)
+        mixedHandler.addPendingMixedTransition(
+            PendingMixedTransition.Minimize(
+                transition = transition,
+                minimizingTask = minimizingTask.taskId,
+                isLastTask = true,
+            )
+        )
+
+        val minimizingTaskChange = createChange(minimizingTask)
+        mixedHandler.startAnimation(
+            transition = transition,
+            info =
+            createTransitionInfo(
+                TRANSIT_TO_BACK,
+                listOf(minimizingTaskChange)
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        verify(transitions)
+            .dispatchTransition(
+                eq(transition),
+                argThat { info -> info.changes.contains(minimizingTaskChange) },
+                any(),
+                any(),
+                any(),
+                eq(mixedHandler)
+            )
+    }
+
     private fun createTransitionInfo(
         type: Int = WindowManager.TRANSIT_CLOSE,
         changeMode: Int = WindowManager.TRANSIT_CLOSE,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 414c1a6..7f790d5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -936,6 +936,28 @@
     }
 
     @Test
+    fun saveBoundsBeforeMinimize_boundsSavedByTaskId() {
+        val taskId = 1
+        val bounds = Rect(0, 0, 200, 200)
+
+        repo.saveBoundsBeforeMinimize(taskId, bounds)
+
+        assertThat(repo.removeBoundsBeforeMinimize(taskId)).isEqualTo(bounds)
+    }
+
+    @Test
+    fun removeBoundsBeforeMinimize_returnsNullAfterBoundsRemoved() {
+        val taskId = 1
+        val bounds = Rect(0, 0, 200, 200)
+        repo.saveBoundsBeforeMinimize(taskId, bounds)
+        repo.removeBoundsBeforeMinimize(taskId)
+
+        val boundsBeforeMinimize = repo.removeBoundsBeforeMinimize(taskId)
+
+        assertThat(boundsBeforeMinimize).isNull()
+    }
+
+    @Test
     fun getExpandedTasksOrdered_returnsFreeformTasksInCorrectOrder() {
         repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true)
         repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 315a46f..ad266ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -3038,6 +3038,21 @@
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+    // Assert event is properly logged
+    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
+      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+      motionEvent,
+      task,
+      displayController
+    )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+      motionEvent,
+      task,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3082,6 +3097,13 @@
       eq(STABLE_BOUNDS),
       anyOrNull(),
     )
+    // Assert no event is logged
+    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
+      any(), any(), any(), any(), any()
+    )
+    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
+      any(), any(), any(), any(), any(), any(), any()
+    )
   }
 
   @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 01b69ae..456b50d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -17,6 +17,7 @@
 package com.android.wm.shell.desktopmode
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
 import android.os.Binder
 import android.os.Handler
 import android.platform.test.annotations.DisableFlags
@@ -24,8 +25,10 @@
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
@@ -63,6 +66,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.any
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.`when`
 import org.mockito.kotlin.eq
@@ -235,6 +239,30 @@
     }
 
     @Test
+    fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() {
+        val bounds = Rect(0, 0, 200, 200)
+        val transition = Binder()
+        val task = setUpFreeformTask()
+        desktopTasksLimiter.addPendingMinimizeChange(
+            transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+
+        val change = TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply {
+            mode = TRANSIT_TO_BACK
+            taskInfo = task
+            setStartAbsBounds(bounds)
+        }
+        desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+            transition,
+            TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
+            StubTransaction() /* startTransaction */,
+            StubTransaction() /* finishTransaction */)
+
+        assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
+        assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)).isEqualTo(
+            bounds)
+    }
+
+    @Test
     fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() {
         val mergedTransition = Binder()
         val newTransition = Binder()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 737439c..7f1c1db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -76,6 +76,7 @@
     private val context = mock<Context>()
     private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
     private val taskRepository = mock<DesktopRepository>()
+    private val mixedHandler = mock<DesktopMixedTransitionHandler>()
 
     private lateinit var transitionObserver: DesktopTasksTransitionObserver
     private lateinit var shellInit: ShellInit
@@ -87,7 +88,7 @@
 
         transitionObserver =
             DesktopTasksTransitionObserver(
-                context, taskRepository, transitions, shellTaskOrganizer, shellInit
+                context, taskRepository, transitions, shellTaskOrganizer, mixedHandler, shellInit
             )
     }
 
@@ -106,6 +107,7 @@
         )
 
         verify(taskRepository).minimizeTask(task.displayId, task.taskId)
+        verify(mixedHandler).addPendingMixedTransition(any())
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
index 72950a8..6d37ed7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -28,8 +28,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.app.AppCompatTaskInfo;
 import android.app.TaskInfo;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
@@ -75,6 +78,7 @@
                 .setContainerLayer()
                 .setName("FakeLeash")
                 .build();
+        mTaskInfo.appCompatTaskInfo = mock(AppCompatTaskInfo.class);
     }
 
     @Test
@@ -93,7 +97,8 @@
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         assertEquals("Expect ANIM_TYPE_BOUNDS animation",
                 animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS);
@@ -107,14 +112,16 @@
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
         oldAnimator.setSurfaceControlTransactionFactory(
                 MockSurfaceControlHelper::createMockSurfaceControlTransaction);
         oldAnimator.start();
 
         final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue2, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         assertEquals("getAnimator with same type returns same animator",
                 oldAnimator, newAnimator);
@@ -145,7 +152,8 @@
         // Fullscreen to PiP.
         PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null,
-                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90);
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90,
+                        false /* alwaysAnimateTaskBounds */);
         // Apply fraction 1 to compute the end value.
         animator.applySurfaceControlTransaction(mLeash, tx, 1);
         final Rect rotatedEndBounds = new Rect(endBounds);
@@ -157,7 +165,8 @@
         startBounds.set(0, 0, 1000, 500);
         endBounds.set(200, 100, 400, 500);
         animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds,
-                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270);
+                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270,
+                false /* alwaysAnimateTaskBounds */);
         animator.applySurfaceControlTransaction(mLeash, tx, 1);
         rotatedEndBounds.set(endBounds);
         rotateBounds(rotatedEndBounds, startBounds, ROTATION_270);
@@ -166,6 +175,37 @@
     }
 
     @Test
+    public void pipTransitionAnimator_rotatedEndValue_overrideMainWindowFrame() {
+        final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction();
+        final Rect startBounds = new Rect(200, 700, 400, 800);
+        final Rect endBounds = new Rect(0, 0, 500, 1000);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500);
+
+        // Fullscreen task to PiP.
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null,
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90,
+                        false /* alwaysAnimateTaskBounds */);
+        // Apply fraction 1 to compute the end value.
+        animator.applySurfaceControlTransaction(mLeash, tx, 1);
+
+        assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame,
+                animator.mCurrentValue);
+
+        // PiP to fullscreen.
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500);
+        startBounds.set(0, 0, 1000, 500);
+        endBounds.set(200, 100, 400, 500);
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds,
+                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270,
+                false /* alwaysAnimateTaskBounds */);
+        animator.applySurfaceControlTransaction(mLeash, tx, 1);
+
+        assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame,
+                animator.mCurrentValue);
+    }
+
+    @Test
     @SuppressWarnings("unchecked")
     public void pipTransitionAnimator_updateEndValue() {
         final Rect baseValue = new Rect(0, 0, 100, 100);
@@ -174,7 +214,8 @@
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         animator.updateEndValue(endValue2);
 
@@ -188,7 +229,8 @@
         final Rect endValue = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
         animator.setSurfaceControlTransactionFactory(
                 MockSurfaceControlHelper::createMockSurfaceControlTransaction);
 
@@ -207,4 +249,126 @@
         verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo),
                 any(SurfaceControl.Transaction.class), eq(animator));
     }
+
+    @Test
+    public void pipTransitionAnimator_overrideMainWindowFrame() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is overridden for in-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getBaseValue());
+        assertEquals("Expect start value is overridden for in-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is overridden for leave-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        true /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                true /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_letterboxed_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityLetterboxed();
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_sizeCompat_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityInSizeCompat();
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index bcb7461..5f58265 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -47,6 +47,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.MockSurfaceControlHelper;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
@@ -61,6 +62,7 @@
 import com.android.wm.shell.common.pip.PipSnapAlgorithm;
 import com.android.wm.shell.common.pip.PipUiEventLogger;
 import com.android.wm.shell.common.pip.SizeSpecSource;
+import com.android.wm.shell.desktopmode.DesktopRepository;
 import com.android.wm.shell.pip.phone.PhonePipMenuController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
@@ -90,6 +92,8 @@
     @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper;
     @Mock private PipUiEventLogger mMockPipUiEventLogger;
     @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen;
+    @Mock private Optional<DesktopRepository> mMockOptionalDesktopRepository;
+    @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     @Mock private ShellTaskOrganizer mMockShellTaskOrganizer;
     @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder;
     private TestShellExecutor mMainExecutor;
@@ -120,8 +124,10 @@
                 mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController,
                 mMockPipSurfaceTransactionHelper, mMockPipTransitionController,
                 mMockPipParamsChangedForwarder, mMockOptionalSplitScreen,
-                Optional.empty() /* pipPerfHintControllerOptional */, mMockDisplayController,
-                mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor);
+                Optional.empty() /* pipPerfHintControllerOptional */,
+                mMockOptionalDesktopRepository, mRootTaskDisplayAreaOrganizer,
+                mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer,
+                mMainExecutor);
         mMainExecutor.flushAll();
         preparePipTaskOrg();
         preparePipSurfaceTransactionHelper();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index 5ebf517..59141ca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -19,6 +19,7 @@
 import android.app.ActivityManager
 import android.app.WindowConfiguration
 import android.content.ComponentName
+import android.graphics.Region
 import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.InsetsState
@@ -33,6 +34,9 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class CaptionWindowDecorationTests : ShellTestCase() {
+
+    private val exclusionRegion = Region.obtain()
+
     @Test
     fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
         val taskInfo = createTaskInfo()
@@ -50,7 +54,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
 
         Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue()
@@ -72,7 +77,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
 
         Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse()
@@ -90,7 +96,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
         Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
         Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 956100d..be664f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -30,6 +30,7 @@
 import android.content.Intent.ACTION_MAIN
 import android.content.pm.ActivityInfo
 import android.graphics.Rect
+import android.graphics.Region
 import android.hardware.display.DisplayManager
 import android.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
@@ -48,6 +49,7 @@
 import android.util.SparseArray
 import android.view.Choreographer
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.ISystemGestureExclusionListener
 import android.view.IWindowManager
 import android.view.InputChannel
 import android.view.InputMonitor
@@ -84,7 +86,6 @@
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.common.MultiInstanceHelper
-import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger
@@ -131,6 +132,7 @@
 import org.mockito.kotlin.KArgumentCaptor
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.argThat
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doNothing
@@ -175,7 +177,7 @@
     @Mock private lateinit var mockInputMonitorFactory:
             DesktopModeWindowDecorViewModel.InputMonitorFactory
     @Mock private lateinit var mockShellController: ShellController
-    @Mock private lateinit var mockShellExecutor: ShellExecutor
+    private val testShellExecutor = TestShellExecutor()
     @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
     @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@@ -230,13 +232,13 @@
 
         spyContext = spy(mContext)
         doNothing().`when`(spyContext).startActivity(any())
-        shellInit = ShellInit(mockShellExecutor)
+        shellInit = ShellInit(testShellExecutor)
         windowDecorByTaskIdSpy.clear()
         spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
         desktopModeEventLogger = mock<DesktopModeEventLogger>()
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
                 spyContext,
-                mockShellExecutor,
+                testShellExecutor,
                 mockMainHandler,
                 mockMainChoreographer,
                 bgExecutor,
@@ -1321,11 +1323,11 @@
 
         decoration.mHasGlobalFocus = true
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, true)
+        verify(decoration).relayout(eq(task), eq(true), anyOrNull())
 
         decoration.mHasGlobalFocus = false
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, false)
+        verify(decoration).relayout(eq(task), eq(false), anyOrNull())
     }
 
     @Test
@@ -1342,17 +1344,66 @@
 
         task.isFocused = true
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, true)
+        verify(decoration).relayout(eq(task), eq(true), anyOrNull())
 
         task.isFocused = false
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, false)
+        verify(decoration).relayout(eq(task), eq(false), anyOrNull())
+    }
+
+    @Test
+    fun testGestureExclusionChanged_updatesDecorations() {
+        val captor = argumentCaptor<ISystemGestureExclusionListener>()
+        verify(mockWindowManager)
+            .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY))
+        val task = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val task2 = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val newRegion = Region.obtain().apply {
+            set(Rect(0, 0, 1600, 80))
+        }
+
+        captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion)
+        testShellExecutor.flushAll()
+
+        verify(task).onExclusionRegionChanged(newRegion)
+        verify(task2).onExclusionRegionChanged(newRegion)
+    }
+
+    @Test
+    fun testGestureExclusionChanged_otherDisplay_skipsDecorationUpdate() {
+        val captor = argumentCaptor<ISystemGestureExclusionListener>()
+        verify(mockWindowManager)
+            .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY))
+        val task = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val task2 = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = 2
+        )
+        val newRegion = Region.obtain().apply {
+            set(Rect(0, 0, 1600, 80))
+        }
+
+        captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion)
+        testShellExecutor.flushAll()
+
+        verify(task).onExclusionRegionChanged(newRegion)
+        verify(task2, never()).onExclusionRegionChanged(newRegion)
     }
 
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         taskSurface: SurfaceControl = SurfaceControl(),
         requestingImmersive: Boolean = false,
+        displayId: Int = DEFAULT_DISPLAY,
         onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
             forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
         onImmersiveOrRestoreListenerCaptor: KArgumentCaptor<() -> Unit> =
@@ -1376,6 +1427,7 @@
     ): DesktopModeWindowDecoration {
         val decor = setUpMockDecorationForTask(createTask(
             windowingMode = windowingMode,
+            displayId = displayId,
             requestingImmersive = requestingImmersive
         ))
         onTaskOpening(decor.mTaskInfo, taskSurface)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 41f57ae..1d2d0f0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -64,6 +64,7 @@
 import android.content.res.TypedArray;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemProperties;
@@ -224,6 +225,7 @@
     private TestableContext mTestableContext;
     private final ShellExecutor mBgExecutor = new TestShellExecutor();
     private final AssistContent mAssistContent = new AssistContent();
+    private final Region mExclusionRegion = Region.obtain();
 
     /** Set up run before test class. */
     @BeforeClass
@@ -262,8 +264,8 @@
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
         when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(),
-                anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(),
-                anyInt(), anyInt()))
+                anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(),
+                anyInt(), anyInt(), anyInt(), anyInt()))
                 .thenReturn(mMockHandleMenu);
         when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false);
         when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
@@ -283,7 +285,7 @@
         final DesktopModeWindowDecoration spyWindowDecor =
                 spy(createWindowDecoration(taskInfo));
 
-        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion);
 
         // Menus should close if open before the task being invisible causes relayout to return.
         verify(spyWindowDecor).closeHandleMenu();
@@ -303,7 +305,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
     }
@@ -324,7 +327,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
     }
@@ -350,7 +354,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
     }
@@ -377,12 +382,14 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -400,12 +407,39 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_freeformAndTransparentAppearance_limitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(
+                APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isTrue();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -422,12 +456,38 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_freeformButOpaqueAppearance_unlimitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_fullscreen_disallowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -443,12 +503,36 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_fullscreen_unlimitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse();
+    }
+
+    @Test
     public void updateRelayoutParams_freeform_inputChannelNeeded() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -464,7 +548,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
     }
@@ -486,7 +571,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
     }
@@ -508,7 +594,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
     }
@@ -531,7 +618,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
     }
@@ -555,7 +643,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
     }
@@ -577,7 +666,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(
                 (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -601,7 +691,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(
                 (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -631,7 +722,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 insetsState,
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         // Takes status bar inset as padding, ignores caption bar inset.
         assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -654,7 +746,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsInsetSource).isFalse();
     }
@@ -676,7 +769,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         // Header is always shown because it's assumed the status bar is always visible.
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -698,7 +792,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
     }
@@ -719,7 +814,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -740,7 +836,8 @@
                 /* isKeyguardVisibleAndOccluded */ true,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -762,7 +859,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
 
@@ -776,7 +874,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -798,7 +897,8 @@
                 /* isKeyguardVisibleAndOccluded */ true,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -809,7 +909,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockTransaction).apply();
         verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any());
@@ -824,7 +924,7 @@
         // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
         taskInfo.isResizeable = false;
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockTransaction, never()).apply();
         verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction);
@@ -836,7 +936,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
     }
@@ -848,7 +948,7 @@
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -865,7 +965,7 @@
         // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
         taskInfo.isResizeable = false;
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
         verify(mMockHandler, never()).post(any());
@@ -877,11 +977,11 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
     }
@@ -892,7 +992,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
@@ -1132,7 +1232,7 @@
         runnableArgument.getValue().run();
 
         // Relayout decor with same captured link
-        decor.relayout(taskInfo, true /* hasGlobalFocus */);
+        decor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         // Verify handle menu's browser link not set to captured link since link is expired
         createHandleMenu(decor);
@@ -1313,7 +1413,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
     }
@@ -1330,7 +1430,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
                 captionStateArgumentCaptor.capture());
@@ -1357,7 +1457,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
                 runnableArgumentCaptor.capture());
         runnableArgumentCaptor.getValue().invoke();
@@ -1380,7 +1480,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
                 captionStateArgumentCaptor.capture());
@@ -1400,7 +1500,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         createHandleMenu(spyWindowDecor);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
@@ -1425,7 +1525,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         createHandleMenu(spyWindowDecor);
         spyWindowDecor.closeHandleMenu();
 
@@ -1440,9 +1540,30 @@
 
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void browserApp_webUriUsedForBrowserApp() {
+        // Make {@link AppToWebUtils#isBrowserApp} return true
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.handleAllWebDataURI = true;
+        resolveInfo.activityInfo = createActivityInfo();
+        when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt()))
+                .thenReturn(List.of(resolveInfo));
+
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
+                TEST_URI3 /* generic link */);
+
+        // Verify web uri used for browser applications
+        createHandleMenu(decor);
+        verifyHandleMenuCreated(TEST_URI2);
+    }
+
+
     private void verifyHandleMenuCreated(@Nullable Uri uri) {
         verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
-                any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
+                any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
                 argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
                 anyInt(), anyInt(), anyInt(), anyInt());
     }
@@ -1522,7 +1643,7 @@
         windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
         windowDecor.mDecorWindowContext = mContext;
         if (relayout) {
-            windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+            windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         }
         return windowDecor;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index ade17c6..7ec2cbf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -242,7 +242,7 @@
 
     private fun createAndShowHandleMenu(
         splitPosition: Int? = null,
-        forceShowSystemBars: Boolean = false,
+        forceShowSystemBars: Boolean = false
     ): HandleMenu {
         val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
             R.layout.desktop_mode_app_header
@@ -266,8 +266,9 @@
             WindowManagerWrapper(mockWindowManager),
             layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true,
             shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false,
-            shouldShowChangeAspectRatioButton = false,
-            null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+            shouldShowChangeAspectRatioButton = false, isBrowserApp = false,
+            null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH,
+            captionHeight = 50,
             captionX = captionX,
             captionY = 0,
         )
@@ -278,7 +279,7 @@
             onNewWindowClickListener = mock(),
             onManageWindowsClickListener = mock(),
             onChangeAspectRatioClickListener = mock(),
-            openInBrowserClickListener = mock(),
+            openInAppOrBrowserClickListener = mock(),
             onOpenByDefaultClickListener = mock(),
             onCloseMenuClickListener = mock(),
             onOutsideTouchListener = mock(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 8e0434c..534803d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -60,6 +60,7 @@
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.DisplayMetrics;
@@ -508,7 +509,7 @@
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
 
         windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */,
-                true /* hasGlobalFocus */);
+                true /* hasGlobalFocus */, Region.obtain());
 
         verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
     }
@@ -525,7 +526,7 @@
         mRelayoutParams.mCaptionTopPadding = 50;
 
         windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */,
-                true /* hasGlobalFocus */);
+                true /* hasGlobalFocus */, Region.obtain());
 
         assertEquals(50, mRelayoutResult.mCaptionTopPadding);
     }
@@ -944,7 +945,7 @@
 
         decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
 
-        verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -958,7 +959,7 @@
 
         decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
 
-        verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -973,7 +974,7 @@
         decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
 
         assertTrue(decor.mIsKeyguardVisibleAndOccluded);
-        verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -987,7 +988,7 @@
 
         decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
 
-        verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     private ActivityManager.RunningTaskInfo createTaskInfo() {
@@ -1061,9 +1062,16 @@
                     surfaceControlViewHostFactory, desktopModeEventLogger);
         }
 
-        @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
-            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus);
+            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus,
+                    Region.obtain());
+        }
+
+        @Override
+        void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+                @NonNull Region displayExclusionRegion) {
+            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus,
+                    displayExclusionRegion);
         }
 
         @Override
@@ -1085,11 +1093,13 @@
         }
 
         void relayout(ActivityManager.RunningTaskInfo taskInfo,
-                boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) {
+                boolean applyStartTransactionOnDraw, boolean hasGlobalFocus,
+                @NonNull Region displayExclusionRegion) {
             mRelayoutParams.mRunningTaskInfo = taskInfo;
             mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
             mRelayoutParams.mLayoutResId = R.layout.caption_layout;
             mRelayoutParams.mHasGlobalFocus = hasGlobalFocus;
+            mRelayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion);
             relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
                     mMockWindowContainerTransaction, mMockView, mRelayoutResult);
         }
diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp
index c6cee07..5ab5a7a 100644
--- a/libs/appfunctions/Android.bp
+++ b/libs/appfunctions/Android.bp
@@ -18,10 +18,10 @@
 }
 
 java_sdk_library {
-    name: "com.google.android.appfunctions.sidecar",
+    name: "com.android.extensions.appfunctions",
     owner: "google",
     srcs: ["java/**/*.java"],
-    api_packages: ["com.google.android.appfunctions.sidecar"],
+    api_packages: ["com.android.extensions.appfunctions"],
     dex_preopt: {
         enabled: false,
     },
@@ -31,9 +31,9 @@
 }
 
 prebuilt_etc {
-    name: "appfunctions.sidecar.xml",
+    name: "appfunctions.extension.xml",
     system_ext_specific: true,
     sub_dir: "permissions",
-    src: "appfunctions.sidecar.xml",
+    src: "appfunctions.extension.xml",
     filename_from_src: true,
 }
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index faf84a8..de40209 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -1,9 +1,29 @@
 // Signature format: 2.0
-package com.google.android.appfunctions.sidecar {
+package com.android.extensions.appfunctions {
+
+  public final class AppFunctionException extends java.lang.Exception {
+    ctor public AppFunctionException(int, @Nullable String);
+    ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle);
+    method public int getErrorCategory();
+    method public int getErrorCode();
+    method @Nullable public String getErrorMessage();
+    method @NonNull public android.os.Bundle getExtras();
+    field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8
+    field public static final int ERROR_CANCELLED = 2001; // 0x7d1
+    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
+    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
+    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
+    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
+    field public static final int ERROR_DENIED = 1000; // 0x3e8
+    field public static final int ERROR_DISABLED = 1002; // 0x3ea
+    field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb
+    field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9
+    field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0
+  }
 
   public final class AppFunctionManager {
     ctor public AppFunctionManager(android.content.Context);
-    method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>);
     method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
     method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>);
     method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>);
@@ -15,7 +35,7 @@
   public abstract class AppFunctionService extends android.app.Service {
     ctor public AppFunctionService();
     method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
-    method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+    method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>);
     field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
     field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
   }
@@ -29,33 +49,17 @@
 
   public static final class ExecuteAppFunctionRequest.Builder {
     ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
-    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build();
-    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
-    method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
+    method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest build();
+    method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
+    method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
   }
 
   public final class ExecuteAppFunctionResponse {
-    method public int getErrorCategory();
-    method @Nullable public String getErrorMessage();
+    ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument);
+    ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument, @NonNull android.os.Bundle);
     method @NonNull public android.os.Bundle getExtras();
-    method public int getResultCode();
     method @NonNull public android.app.appsearch.GenericDocument getResultDocument();
-    method public boolean isSuccess();
-    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle);
-    method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
-    field public static final int ERROR_CATEGORY_APP = 3; // 0x3
-    field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1
-    field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2
-    field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0
-    field public static final String PROPERTY_RETURN_VALUE = "returnValue";
-    field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8
-    field public static final int RESULT_CANCELLED = 2001; // 0x7d1
-    field public static final int RESULT_DENIED = 1000; // 0x3e8
-    field public static final int RESULT_DISABLED = 1002; // 0x3ea
-    field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb
-    field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9
-    field public static final int RESULT_OK = 0; // 0x0
-    field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0
+    field public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue";
   }
 
 }
diff --git a/libs/appfunctions/appfunctions.sidecar.xml b/libs/appfunctions/appfunctions.extension.xml
similarity index 83%
rename from libs/appfunctions/appfunctions.sidecar.xml
rename to libs/appfunctions/appfunctions.extension.xml
index bef8b6e..dd09cc3 100644
--- a/libs/appfunctions/appfunctions.sidecar.xml
+++ b/libs/appfunctions/appfunctions.extension.xml
@@ -16,6 +16,6 @@
   -->
 <permissions>
     <library
-        name="com.google.android.appfunctions.sidecar"
-        file="/system_ext/framework/com.google.android.appfunctions.sidecar.jar"/>
+        name="com.android.extensions.appfunctions"
+        file="/system_ext/framework/com.android.extensions.appfunctions.jar"/>
 </permissions>
\ No newline at end of file
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java
new file mode 100644
index 0000000..2540236
--- /dev/null
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java
@@ -0,0 +1,212 @@
+/*
+ * 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.extensions.appfunctions;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Represents an app function related errors. */
+public final class AppFunctionException extends Exception {
+    /**
+     * The caller does not have the permission to execute an app function.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_DENIED = 1000;
+
+    /**
+     * The caller supplied invalid arguments to the execution request.
+     *
+     * <p>This error may be considered similar to {@link IllegalArgumentException}.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_INVALID_ARGUMENT = 1001;
+
+    /**
+     * The caller tried to execute a disabled app function.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_DISABLED = 1002;
+
+    /**
+     * The caller tried to execute a function that does not exist.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
+     */
+    public static final int ERROR_FUNCTION_NOT_FOUND = 1003;
+
+    /**
+     * An internal unexpected error coming from the system.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
+     */
+    public static final int ERROR_SYSTEM_ERROR = 2000;
+
+    /**
+     * The operation was cancelled. Use this error code to report that a cancellation is done after
+     * receiving a cancellation signal.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
+     */
+    public static final int ERROR_CANCELLED = 2001;
+
+    /**
+     * An unknown error occurred while processing the call in the AppFunctionService.
+     *
+     * <p>This error is thrown when the service is connected in the remote application but an
+     * unexpected error is thrown from the bound application.
+     *
+     * <p>This error is in the {@link #ERROR_CATEGORY_APP} category.
+     */
+    public static final int ERROR_APP_UNKNOWN_ERROR = 3000;
+
+    /**
+     * The error category is unknown.
+     *
+     * <p>This is the default value for {@link #getErrorCategory}.
+     */
+    public static final int ERROR_CATEGORY_UNKNOWN = 0;
+
+    /**
+     * The error is caused by the app requesting a function execution.
+     *
+     * <p>For example, the caller provided invalid parameters in the execution request e.g. an
+     * invalid function ID.
+     *
+     * <p>Errors in the category fall in the range 1000-1999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_REQUEST_ERROR = 1;
+
+    /**
+     * The error is caused by an issue in the system.
+     *
+     * <p>For example, the AppFunctionService implementation is not found by the system.
+     *
+     * <p>Errors in the category fall in the range 2000-2999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_SYSTEM = 2;
+
+    /**
+     * The error is caused by the app providing the function.
+     *
+     * <p>For example, the app crashed when the system is executing the request.
+     *
+     * <p>Errors in the category fall in the range 3000-3999 inclusive.
+     */
+    public static final int ERROR_CATEGORY_APP = 3;
+
+    private final int mErrorCode;
+    @Nullable private final String mErrorMessage;
+    @NonNull private final Bundle mExtras;
+
+    public AppFunctionException(int errorCode, @Nullable String errorMessage) {
+        this(errorCode, errorMessage, Bundle.EMPTY);
+    }
+
+    public AppFunctionException(
+            int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) {
+        super(errorMessage);
+        mErrorCode = errorCode;
+        mErrorMessage = errorMessage;
+        mExtras = extras;
+    }
+
+    /** Returns one of the {@code ERROR} constants. */
+    @ErrorCode
+    public int getErrorCode() {
+        return mErrorCode;
+    }
+
+    /** Returns the error message. */
+    @Nullable
+    public String getErrorMessage() {
+        return mErrorMessage;
+    }
+
+    /**
+     * Returns the error category.
+     *
+     * <p>This method categorizes errors based on their underlying cause, allowing developers to
+     * implement targeted error handling and provide more informative error messages to users. It
+     * maps ranges of error codes to specific error categories.
+     *
+     * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the error code does not belong to
+     * any error category.
+     *
+     * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding
+     * error code ranges.
+     */
+    @ErrorCategory
+    public int getErrorCategory() {
+        if (mErrorCode >= 1000 && mErrorCode < 2000) {
+            return ERROR_CATEGORY_REQUEST_ERROR;
+        }
+        if (mErrorCode >= 2000 && mErrorCode < 3000) {
+            return ERROR_CATEGORY_SYSTEM;
+        }
+        if (mErrorCode >= 3000 && mErrorCode < 4000) {
+            return ERROR_CATEGORY_APP;
+        }
+        return ERROR_CATEGORY_UNKNOWN;
+    }
+
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Error codes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"ERROR_"},
+            value = {
+                ERROR_DENIED,
+                ERROR_APP_UNKNOWN_ERROR,
+                ERROR_FUNCTION_NOT_FOUND,
+                ERROR_SYSTEM_ERROR,
+                ERROR_INVALID_ARGUMENT,
+                ERROR_DISABLED,
+                ERROR_CANCELLED
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCode {}
+
+    /**
+     * Error categories.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"ERROR_CATEGORY_"},
+            value = {
+                ERROR_CATEGORY_UNKNOWN,
+                ERROR_CATEGORY_REQUEST_ERROR,
+                ERROR_CATEGORY_APP,
+                ERROR_CATEGORY_SYSTEM
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCategory {}
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java
similarity index 88%
rename from libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
rename to libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java
index 2075104..9eb66a3 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.android.appfunctions.sidecar;
+package com.android.extensions.appfunctions;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
@@ -31,7 +31,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 /**
  * Provides app functions related functionalities.
@@ -115,7 +114,9 @@
             @NonNull ExecuteAppFunctionRequest sidecarRequest,
             @NonNull @CallbackExecutor Executor executor,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+            @NonNull
+                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
+                            callback) {
         Objects.requireNonNull(sidecarRequest);
         Objects.requireNonNull(executor);
         Objects.requireNonNull(callback);
@@ -126,10 +127,20 @@
                 platformRequest,
                 executor,
                 cancellationSignal,
-                (platformResponse) -> {
-                    callback.accept(
-                            SidecarConverter.getSidecarExecuteAppFunctionResponse(
-                                    platformResponse));
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(
+                            android.app.appfunctions.ExecuteAppFunctionResponse result) {
+                        callback.onResult(
+                                SidecarConverter.getSidecarExecuteAppFunctionResponse(result));
+                    }
+
+                    @Override
+                    public void onError(
+                            android.app.appfunctions.AppFunctionException exception) {
+                        callback.onError(
+                                SidecarConverter.getSidecarAppFunctionException(exception));
+                    }
                 });
     }
 
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
similarity index 79%
rename from libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
rename to libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
index 0dc87e4..55f5791 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.google.android.appfunctions.sidecar;
+package com.android.extensions.appfunctions;
 
-import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
+import static com.android.extensions.appfunctions.SidecarConverter.getPlatformAppFunctionException;
+import static com.android.extensions.appfunctions.SidecarConverter.getPlatformExecuteAppFunctionResponse;
 
 import android.annotation.MainThread;
 import android.annotation.NonNull;
@@ -26,9 +27,7 @@
 import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.IBinder;
-import android.util.Log;
-
-import java.util.function.Consumer;
+import android.os.OutcomeReceiver;
 
 /**
  * Abstract base class to provide app functions to the system.
@@ -80,10 +79,18 @@
                                         platformRequest),
                                 callingPackage,
                                 cancellationSignal,
-                                (sidecarResponse) -> {
-                                    callback.accept(
-                                            SidecarConverter.getPlatformExecuteAppFunctionResponse(
-                                                    sidecarResponse));
+                                new OutcomeReceiver<>() {
+                                    @Override
+                                    public void onResult(ExecuteAppFunctionResponse result) {
+                                        callback.onResult(
+                                                getPlatformExecuteAppFunctionResponse(result));
+                                    }
+
+                                    @Override
+                                    public void onError(AppFunctionException exception) {
+                                        callback.onError(
+                                                getPlatformAppFunctionException(exception));
+                                    }
                                 });
                     });
 
@@ -116,12 +123,14 @@
      * @param request The function execution request.
      * @param callingPackage The package name of the app that is requesting the execution.
      * @param cancellationSignal A signal to cancel the execution.
-     * @param callback A callback to report back the result.
+     * @param callback A callback to report back the result or error.
      */
     @MainThread
     public abstract void onExecuteFunction(
             @NonNull ExecuteAppFunctionRequest request,
             @NonNull String callingPackage,
             @NonNull CancellationSignal cancellationSignal,
-            @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+            @NonNull
+                    OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>
+                            callback);
 }
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java
similarity index 96%
rename from libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
rename to libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java
index 593c521..baddc24 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.android.appfunctions.sidecar;
+package com.android.extensions.appfunctions;
 
 import android.annotation.NonNull;
 import android.app.appsearch.GenericDocument;
@@ -91,8 +91,8 @@
      * Returns the function parameters. The key is the parameter name, and the value is the
      * parameter value.
      *
-     * <p>The bundle may have missing parameters. Developers are advised to implement defensive
-     * handling measures.
+     * <p>The {@link GenericDocument} may have missing parameters. Developers are advised to
+     * implement defensive handling measures.
      *
      * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be
      * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java
new file mode 100644
index 0000000..0826f04
--- /dev/null
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java
@@ -0,0 +1,103 @@
+/*
+ * 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.extensions.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appfunctions.AppFunctionManager;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/** The response to an app function execution. */
+public final class ExecuteAppFunctionResponse {
+    /**
+     * The name of the property that stores the function return value within the {@code
+     * resultDocument}.
+     *
+     * <p>See {@link GenericDocument#getProperty(String)} for more information.
+     *
+     * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will
+     * be empty {@link GenericDocument}.
+     *
+     * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will
+     * return {@code null}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue";
+
+    /**
+     * Returns the return value of the executed function.
+     *
+     * <p>The return value is stored in a {@link GenericDocument} with the key {@link
+     * #PROPERTY_RETURN_VALUE}.
+     *
+     * <p>See {@link #getResultDocument} for more information on extracting the return value.
+     */
+    @NonNull private final GenericDocument mResultDocument;
+
+    /** Returns the additional metadata data relevant to this function execution response. */
+    @NonNull private final Bundle mExtras;
+
+    /**
+     * @param resultDocument The return value of the executed function.
+     */
+    public ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument) {
+        this(resultDocument, Bundle.EMPTY);
+    }
+
+    /**
+     * @param resultDocument The return value of the executed function.
+     * @param extras The additional metadata for this function execution response.
+     */
+    public ExecuteAppFunctionResponse(
+            @NonNull GenericDocument resultDocument, @NonNull Bundle extras) {
+        mResultDocument = Objects.requireNonNull(resultDocument);
+        mExtras = Objects.requireNonNull(extras);
+    }
+
+    /**
+     * Returns a generic document containing the return value of the executed function.
+     *
+     * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
+     *
+     * <p>Sample code for extracting the return value:
+     *
+     * <pre>
+     *     GenericDocument resultDocument = response.getResultDocument();
+     *     Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE);
+     *     if (returnValue != null) {
+     *       // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString},
+     *       // {@link GenericDocument#getPropertyLong} etc.
+     *       // Do something with the returnValue
+     *     }
+     * </pre>
+     *
+     * @see AppFunctionManager on how to determine the expected function return.
+     */
+    @NonNull
+    public GenericDocument getResultDocument() {
+        return mResultDocument;
+    }
+
+    /** Returns the additional metadata for this function execution response. */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+}
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java
new file mode 100644
index 0000000..5e1fc7e
--- /dev/null
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java
@@ -0,0 +1,112 @@
+/*
+ * 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.extensions.appfunctions;
+
+import android.annotation.NonNull;
+
+/**
+ * Utility class containing methods to convert Sidecar objects of AppFunctions API into the
+ * underlying platform classes.
+ *
+ * @hide
+ */
+public final class SidecarConverter {
+    private SidecarConverter() {}
+
+    /**
+     * Converts sidecar's {@link ExecuteAppFunctionRequest} into platform's {@link
+     * android.app.appfunctions.ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionRequest
+            getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) {
+        return new android.app.appfunctions.ExecuteAppFunctionRequest.Builder(
+                        request.getTargetPackageName(), request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts sidecar's {@link ExecuteAppFunctionResponse} into platform's {@link
+     * android.app.appfunctions.ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.ExecuteAppFunctionResponse
+            getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) {
+        return new android.app.appfunctions.ExecuteAppFunctionResponse(
+                response.getResultDocument(), response.getExtras());
+    }
+
+    /**
+     * Converts sidecar's {@link AppFunctionException} into platform's {@link
+     * android.app.appfunctions.AppFunctionException}
+     *
+     * @hide
+     */
+    @NonNull
+    public static android.app.appfunctions.AppFunctionException
+            getPlatformAppFunctionException(@NonNull AppFunctionException exception) {
+        return new android.app.appfunctions.AppFunctionException(
+                exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras());
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} into sidecar's
+     * {@link ExecuteAppFunctionRequest}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) {
+        return new ExecuteAppFunctionRequest.Builder(
+                        request.getTargetPackageName(), request.getFunctionIdentifier())
+                .setExtras(request.getExtras())
+                .setParameters(request.getParameters())
+                .build();
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} into
+     * sidecar's {@link ExecuteAppFunctionResponse}
+     *
+     * @hide
+     */
+    @NonNull
+    public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse(
+            @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) {
+        return new ExecuteAppFunctionResponse(response.getResultDocument(), response.getExtras());
+    }
+
+    /**
+     * Converts platform's {@link android.app.appfunctions.AppFunctionException} into
+     * sidecar's {@link AppFunctionException}
+     *
+     * @hide
+     */
+    @NonNull
+    public static AppFunctionException getSidecarAppFunctionException(
+            @NonNull android.app.appfunctions.AppFunctionException exception) {
+        return new AppFunctionException(
+                exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras());
+    }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
deleted file mode 100644
index 4e88fb0..0000000
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ /dev/null
@@ -1,359 +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.google.android.appfunctions.sidecar;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.appsearch.GenericDocument;
-import android.os.Bundle;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Objects;
-
-/**
- * The response to an app function execution.
- *
- * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel
- * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
- * platform API).
- */
-public final class ExecuteAppFunctionResponse {
-    /**
-     * The name of the property that stores the function return value within the {@code
-     * resultDocument}.
-     *
-     * <p>See {@link GenericDocument#getProperty(String)} for more information.
-     *
-     * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will
-     * be empty {@link GenericDocument}.
-     *
-     * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will
-     * return {@code null}.
-     *
-     * <p>See {@link #getResultDocument} for more information on extracting the return value.
-     */
-    public static final String PROPERTY_RETURN_VALUE = "returnValue";
-
-    /**
-     * The call was successful.
-     *
-     * <p>This result code does not belong in an error category.
-     */
-    public static final int RESULT_OK = 0;
-
-    /**
-     * The caller does not have the permission to execute an app function.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_DENIED = 1000;
-
-    /**
-     * The caller supplied invalid arguments to the execution request.
-     *
-     * <p>This error may be considered similar to {@link IllegalArgumentException}.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_INVALID_ARGUMENT = 1001;
-
-    /**
-     * The caller tried to execute a disabled app function.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_DISABLED = 1002;
-
-    /**
-     * The caller tried to execute a function that does not exist.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category.
-     */
-    public static final int RESULT_FUNCTION_NOT_FOUND = 1003;
-
-    /**
-     * An internal unexpected error coming from the system.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
-     */
-    public static final int RESULT_SYSTEM_ERROR = 2000;
-
-    /**
-     * The operation was cancelled. Use this error code to report that a cancellation is done after
-     * receiving a cancellation signal.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category.
-     */
-    public static final int RESULT_CANCELLED = 2001;
-
-    /**
-     * An unknown error occurred while processing the call in the AppFunctionService.
-     *
-     * <p>This error is thrown when the service is connected in the remote application but an
-     * unexpected error is thrown from the bound application.
-     *
-     * <p>This error is in the {@link #ERROR_CATEGORY_APP} category.
-     */
-    public static final int RESULT_APP_UNKNOWN_ERROR = 3000;
-
-    /**
-     * The error category is unknown.
-     *
-     * <p>This is the default value for {@link #getErrorCategory}.
-     */
-    public static final int ERROR_CATEGORY_UNKNOWN = 0;
-
-    /**
-     * The error is caused by the app requesting a function execution.
-     *
-     * <p>For example, the caller provided invalid parameters in the execution request e.g. an
-     * invalid function ID.
-     *
-     * <p>Errors in the category fall in the range 1000-1999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_REQUEST_ERROR = 1;
-
-    /**
-     * The error is caused by an issue in the system.
-     *
-     * <p>For example, the AppFunctionService implementation is not found by the system.
-     *
-     * <p>Errors in the category fall in the range 2000-2999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_SYSTEM = 2;
-
-    /**
-     * The error is caused by the app providing the function.
-     *
-     * <p>For example, the app crashed when the system is executing the request.
-     *
-     * <p>Errors in the category fall in the range 3000-3999 inclusive.
-     */
-    public static final int ERROR_CATEGORY_APP = 3;
-
-    /** The result code of the app function execution. */
-    @ResultCode private final int mResultCode;
-
-    /**
-     * The error message associated with the result, if any. This is {@code null} if the result code
-     * is {@link #RESULT_OK}.
-     */
-    @Nullable private final String mErrorMessage;
-
-    /**
-     * Returns the return value of the executed function.
-     *
-     * <p>The return value is stored in a {@link GenericDocument} with the key {@link
-     * #PROPERTY_RETURN_VALUE}.
-     *
-     * <p>See {@link #getResultDocument} for more information on extracting the return value.
-     */
-    @NonNull private final GenericDocument mResultDocument;
-
-    /** Returns the additional metadata data relevant to this function execution response. */
-    @NonNull private final Bundle mExtras;
-
-    private ExecuteAppFunctionResponse(
-            @NonNull GenericDocument resultDocument,
-            @NonNull Bundle extras,
-            @ResultCode int resultCode,
-            @Nullable String errorMessage) {
-        mResultDocument = Objects.requireNonNull(resultDocument);
-        mExtras = Objects.requireNonNull(extras);
-        mResultCode = resultCode;
-        mErrorMessage = errorMessage;
-    }
-
-    /**
-     * Returns result codes from throwable.
-     *
-     * @hide
-     */
-    static @ResultCode int getResultCode(@NonNull Throwable t) {
-        if (t instanceof IllegalArgumentException) {
-            return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
-        }
-        return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
-    }
-
-    /**
-     * Returns a successful response.
-     *
-     * @param resultDocument The return value of the executed function.
-     * @param extras The additional metadata data relevant to this function execution response.
-     */
-    @NonNull
-    public static ExecuteAppFunctionResponse newSuccess(
-            @NonNull GenericDocument resultDocument, @Nullable Bundle extras) {
-        Objects.requireNonNull(resultDocument);
-        Bundle actualExtras = getActualExtras(extras);
-
-        return new ExecuteAppFunctionResponse(
-                resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null);
-    }
-
-    /**
-     * Returns a failure response.
-     *
-     * @param resultCode The result code of the app function execution.
-     * @param extras The additional metadata data relevant to this function execution response.
-     * @param errorMessage The error message associated with the result, if any.
-     */
-    @NonNull
-    public static ExecuteAppFunctionResponse newFailure(
-            @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) {
-        if (resultCode == RESULT_OK) {
-            throw new IllegalArgumentException("resultCode must not be RESULT_OK");
-        }
-        Bundle actualExtras = getActualExtras(extras);
-        GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build();
-        return new ExecuteAppFunctionResponse(
-                emptyDocument, actualExtras, resultCode, errorMessage);
-    }
-
-    private static Bundle getActualExtras(@Nullable Bundle extras) {
-        if (extras == null) {
-            return Bundle.EMPTY;
-        }
-        return extras;
-    }
-
-    /**
-     * Returns the error category of the {@link ExecuteAppFunctionResponse}.
-     *
-     * <p>This method categorizes errors based on their underlying cause, allowing developers to
-     * implement targeted error handling and provide more informative error messages to users. It
-     * maps ranges of result codes to specific error categories.
-     *
-     * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to
-     * ensure correct categorization of the failed response.
-     *
-     * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to
-     * any error category, for example, in the case of a successful result with {@link #RESULT_OK}.
-     *
-     * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding
-     * result code ranges.
-     */
-    @ErrorCategory
-    public int getErrorCategory() {
-        if (mResultCode >= 1000 && mResultCode < 2000) {
-            return ERROR_CATEGORY_REQUEST_ERROR;
-        }
-        if (mResultCode >= 2000 && mResultCode < 3000) {
-            return ERROR_CATEGORY_SYSTEM;
-        }
-        if (mResultCode >= 3000 && mResultCode < 4000) {
-            return ERROR_CATEGORY_APP;
-        }
-        return ERROR_CATEGORY_UNKNOWN;
-    }
-
-    /**
-     * Returns a generic document containing the return value of the executed function.
-     *
-     * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
-     *
-     * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed
-     * function does not produce a return value.
-     *
-     * <p>Sample code for extracting the return value:
-     *
-     * <pre>
-     *     GenericDocument resultDocument = response.getResultDocument();
-     *     Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE);
-     *     if (returnValue != null) {
-     *       // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString},
-     *       // {@link GenericDocument#getPropertyLong} etc.
-     *       // Do something with the returnValue
-     *     }
-     * </pre>
-     */
-    @NonNull
-    public GenericDocument getResultDocument() {
-        return mResultDocument;
-    }
-
-    /** Returns the extras of the app function execution. */
-    @NonNull
-    public Bundle getExtras() {
-        return mExtras;
-    }
-
-    /**
-     * Returns {@code true} if {@link #getResultCode} equals {@link
-     * ExecuteAppFunctionResponse#RESULT_OK}.
-     */
-    public boolean isSuccess() {
-        return getResultCode() == RESULT_OK;
-    }
-
-    /**
-     * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}.
-     */
-    @ResultCode
-    public int getResultCode() {
-        return mResultCode;
-    }
-
-    /**
-     * Returns the error message associated with this result.
-     *
-     * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
-     */
-    @Nullable
-    public String getErrorMessage() {
-        return mErrorMessage;
-    }
-
-    /**
-     * Result codes.
-     *
-     * @hide
-     */
-    @IntDef(
-            prefix = {"RESULT_"},
-            value = {
-                RESULT_OK,
-                RESULT_DENIED,
-                RESULT_APP_UNKNOWN_ERROR,
-                RESULT_SYSTEM_ERROR,
-                RESULT_FUNCTION_NOT_FOUND,
-                RESULT_INVALID_ARGUMENT,
-                RESULT_DISABLED,
-                RESULT_CANCELLED
-            })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ResultCode {}
-
-    /**
-     * Error categories.
-     *
-     * @hide
-     */
-    @IntDef(
-            prefix = {"ERROR_CATEGORY_"},
-            value = {
-                ERROR_CATEGORY_UNKNOWN,
-                ERROR_CATEGORY_REQUEST_ERROR,
-                ERROR_CATEGORY_APP,
-                ERROR_CATEGORY_SYSTEM
-            })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ErrorCategory {}
-}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
deleted file mode 100644
index b1b05f7..0000000
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
+++ /dev/null
@@ -1,104 +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.google.android.appfunctions.sidecar;
-
-import android.annotation.NonNull;
-
-/**
- * Utility class containing methods to convert Sidecar objects of AppFunctions API into the
- * underlying platform classes.
- *
- * @hide
- */
-public final class SidecarConverter {
-    private SidecarConverter() {}
-
-    /**
-     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
-     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
-     *
-     * @hide
-     */
-    @NonNull
-    public static android.app.appfunctions.ExecuteAppFunctionRequest
-            getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) {
-        return new
-                android.app.appfunctions.ExecuteAppFunctionRequest.Builder(
-                request.getTargetPackageName(),
-                request.getFunctionIdentifier())
-                .setExtras(request.getExtras())
-                .setParameters(request.getParameters())
-                .build();
-    }
-
-    /**
-     * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
-     * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
-     *
-     * @hide
-     */
-    @NonNull
-    public static android.app.appfunctions.ExecuteAppFunctionResponse
-            getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) {
-        if (response.isSuccess()) {
-            return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess(
-                    response.getResultDocument(), response.getExtras());
-        } else {
-            return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure(
-                    response.getResultCode(),
-                    response.getErrorMessage(),
-                    response.getExtras());
-        }
-    }
-
-    /**
-     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
-     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
-     *
-     * @hide
-     */
-    @NonNull
-    public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest(
-            @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) {
-        return new ExecuteAppFunctionRequest.Builder(
-                request.getTargetPackageName(),
-                request.getFunctionIdentifier())
-                .setExtras(request.getExtras())
-                .setParameters(request.getParameters())
-                .build();
-    }
-
-    /**
-     * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
-     * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
-     *
-     * @hide
-     */
-    @NonNull
-    public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse(
-            @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) {
-        if (response.isSuccess()) {
-            return ExecuteAppFunctionResponse.newSuccess(
-                    response.getResultDocument(), response.getExtras());
-        } else {
-            return ExecuteAppFunctionResponse.newFailure(
-                    response.getResultCode(),
-                    response.getErrorMessage(),
-                    response.getExtras());
-        }
-    }
-}
diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp
index 6f5eff3..db79675 100644
--- a/libs/appfunctions/tests/Android.bp
+++ b/libs/appfunctions/tests/Android.bp
@@ -25,7 +25,7 @@
         "androidx.test.rules",
         "androidx.test.ext.junit",
         "androidx.core_core-ktx",
-        "com.google.android.appfunctions.sidecar.impl",
+        "com.android.extensions.appfunctions.impl",
         "junit",
         "kotlin-test",
         "mockito-target-extended-minus-junit4",
diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt
similarity index 62%
rename from libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
rename to libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt
index 264f842..11202d5 100644
--- a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
+++ b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt
@@ -14,14 +14,15 @@
  * limitations under the License.
  */
 
-package com.google.android.appfunctions.sidecar.tests
+package com.android.extensions.appfunctions.tests
 
+import android.app.appfunctions.AppFunctionException
 import android.app.appfunctions.ExecuteAppFunctionRequest
 import android.app.appfunctions.ExecuteAppFunctionResponse
 import android.app.appsearch.GenericDocument
 import android.os.Bundle
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.android.appfunctions.sidecar.SidecarConverter
+import com.android.extensions.appfunctions.SidecarConverter
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -60,7 +61,7 @@
                 .setPropertyLong("testLong", 23)
                 .build()
         val sidecarRequest =
-            com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder(
+            com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder(
                 "targetPkg",
                 "targetFunctionId"
             )
@@ -83,44 +84,38 @@
             GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
                 .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
                 .build()
-        val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null)
+        val platformResponse = ExecuteAppFunctionResponse(resultGd)
 
         val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
             platformResponse
         )
 
-        assertThat(sidecarResponse.isSuccess).isTrue()
         assertThat(
             sidecarResponse.resultDocument.getProperty(
                 ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
             )
         )
             .isEqualTo(booleanArrayOf(true))
-        assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
-        assertThat(sidecarResponse.errorMessage).isNull()
     }
 
     @Test
-    fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() {
-        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
-        val platformResponse =
-            ExecuteAppFunctionResponse.newFailure(
-                ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR,
-                null,
-                null
+    fun getSidecarAppFunctionException_sameContents() {
+        val bundle = Bundle()
+        bundle.putString("key", "value")
+        val platformException =
+            AppFunctionException(
+                AppFunctionException.ERROR_SYSTEM_ERROR,
+                "error",
+                bundle
             )
 
-        val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
-            platformResponse
+        val sidecarException = SidecarConverter.getSidecarAppFunctionException(
+            platformException
         )
 
-        assertThat(sidecarResponse.isSuccess).isFalse()
-        assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
-        assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id)
-        assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
-        assertThat(sidecarResponse.resultCode)
-            .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR)
-        assertThat(sidecarResponse.errorMessage).isNull()
+        assertThat(sidecarException.errorCode).isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR)
+        assertThat(sidecarException.errorMessage).isEqualTo("error")
+        assertThat(sidecarException.extras.getString("key")).isEqualTo("value")
     }
 
     @Test
@@ -129,44 +124,39 @@
             GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
                 .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
                 .build()
-        val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse
-            .newSuccess(resultGd, null)
+        val sidecarResponse =
+            com.android.extensions.appfunctions.ExecuteAppFunctionResponse(resultGd)
 
         val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
             sidecarResponse
         )
 
-        assertThat(platformResponse.isSuccess).isTrue()
         assertThat(
             platformResponse.resultDocument.getProperty(
                 ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
             )
         )
             .isEqualTo(booleanArrayOf(true))
-        assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
-        assertThat(platformResponse.errorMessage).isNull()
     }
 
     @Test
-    fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() {
-        val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
-        val sidecarResponse =
-            com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure(
-                ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR,
-                null,
-                null
+    fun getPlatformAppFunctionException_sameContents() {
+        val bundle = Bundle()
+        bundle.putString("key", "value")
+        val sidecarException =
+            com.android.extensions.appfunctions.AppFunctionException(
+                AppFunctionException.ERROR_SYSTEM_ERROR,
+                "error",
+                bundle
             )
 
-        val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
-            sidecarResponse
+        val platformException = SidecarConverter.getPlatformAppFunctionException(
+            sidecarException
         )
 
-        assertThat(platformResponse.isSuccess).isFalse()
-        assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
-        assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id)
-        assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
-        assertThat(platformResponse.resultCode)
-            .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR)
-        assertThat(platformResponse.errorMessage).isNull()
+        assertThat(platformException.errorCode)
+            .isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR)
+        assertThat(platformException.errorMessage).isEqualTo("error")
+        assertThat(platformException.extras.getString("key")).isEqualTo("value")
     }
 }
diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h
index fddcf29..5f84f47 100644
--- a/libs/hwui/FeatureFlags.h
+++ b/libs/hwui/FeatureFlags.h
@@ -33,9 +33,9 @@
 #endif  // __ANDROID__
 }
 
-inline bool typeface_redesign() {
+inline bool typeface_redesign_readonly() {
 #ifdef __ANDROID__
-    static bool flag = com_android_text_flags_typeface_redesign();
+    static bool flag = com_android_text_flags_typeface_redesign_readonly();
     return flag;
 #else
     return true;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 5ad788c..fa27af6 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -154,3 +154,13 @@
   description: "API's that enable animated image drawables to use nearest sampling when scaling."
   bug: "370523334"
 }
+
+flag {
+  name: "remove_vri_sketchy_destroy"
+  namespace: "core_graphics"
+  description: "Remove the eager yet thread-violating destroyHardwareResources in VRI#die"
+  bug: "377057106"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index 1510ce1..20acf98 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -73,7 +73,7 @@
     static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) {
         float saveSkewX = paint->getSkFont().getSkewX();
         bool savefakeBold = paint->getSkFont().isEmbolden();
-        if (text_feature::typeface_redesign()) {
+        if (text_feature::typeface_redesign_readonly()) {
             for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) {
                 uint32_t start = layout.getFontRunStart(runIdx);
                 uint32_t end = layout.getFontRunEnd(runIdx);
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index 70e6bed..5f69346 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -86,7 +86,7 @@
         overallDescent = std::max(overallDescent, extent.descent);
     }
 
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         uint32_t runCount = layout.getFontRunCount();
 
         std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds;
@@ -229,7 +229,7 @@
 // CriticalNative
 static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         float value =
                 findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght);
         return std::isnan(value) ? NO_OVERRIDE : value;
@@ -241,7 +241,7 @@
 // CriticalNative
 static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         float value =
                 findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital);
         return std::isnan(value) ? NO_OVERRIDE : value;
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 88efed5..3f9126a 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -1000,7 +1000,10 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public boolean updateResourcePriority(int priority, int niceValue) {
-        return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
+        if (mTunerResourceManager != null) {
+            return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
+        }
+        return false;
     }
 
     /**
@@ -1017,7 +1020,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public void setResourceHolderRetain(boolean resourceHolderRetain) {
-        mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+        if (mTunerResourceManager != null) {
+            mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+        }
     }
 
     IHwBinder getBinder() {
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index e575dae..2ae89d3 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -18,6 +18,7 @@
 
 import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
 import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
+import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS;
 
 import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
 
@@ -890,7 +891,7 @@
  any start codes), and submit it as a <strong>regular</strong> input buffer.
  <p>
  You will receive an {@link #INFO_OUTPUT_FORMAT_CHANGED} return value from {@link
- #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputBufferAvailable
+ #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputFormatChanged
  onOutputFormatChanged} callback just after the picture-size change takes place and before any
  frames with the new size have been returned.
  <p class=note>
@@ -1835,6 +1836,13 @@
     private static final int CB_CRYPTO_ERROR = 6;
     private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7;
 
+    /**
+     * Callback ID for when the metrics for this codec have been flushed due to
+     * the start of a new subsession. The associated Java Message object will
+     * contain the flushed metrics as a PersistentBundle in the obj field.
+     */
+    private static final int CB_METRICS_FLUSHED = 8;
+
     private class EventHandler extends Handler {
         private MediaCodec mCodec;
 
@@ -2007,6 +2015,15 @@
                     break;
                 }
 
+                case CB_METRICS_FLUSHED:
+                {
+
+                    if (GetFlag(() -> android.media.codec.Flags.subsessionMetrics())) {
+                        mCallback.onMetricsFlushed(mCodec, (PersistableBundle)msg.obj);
+                    }
+                    break;
+                }
+
                 default:
                 {
                     break;
@@ -4958,14 +4975,24 @@
     public native final String getCanonicalName();
 
     /**
-     *  Return Metrics data about the current codec instance.
+     * Return Metrics data about the current codec instance.
+     * <p>
+     * Call this method after configuration, during execution, or after
+     * the codec has been already stopped.
+     * <p>
+     * Beginning with {@link android.os.Build.VERSION_CODES#B}
+     * this method can be used to get the Metrics data prior to an error.
+     * (e.g. in {@link Callback#onError} or after a method throws
+     * {@link MediaCodec.CodecException}.) Before that, the Metrics data was
+     * cleared on error, resulting in a null return value.
      *
      * @return a {@link PersistableBundle} containing the set of attributes and values
      * available for the media being handled by this instance of MediaCodec
      * The attributes are descibed in {@link MetricsConstants}.
      *
      * Additional vendor-specific fields may also be present in
-     * the return value.
+     * the return value. Returns null if there is no Metrics data.
+     *
      */
     public PersistableBundle getMetrics() {
         PersistableBundle bundle = native_getMetrics();
@@ -5692,6 +5719,27 @@
          */
         public abstract void onOutputFormatChanged(
                 @NonNull MediaCodec codec, @NonNull MediaFormat format);
+
+        /**
+         * Called when the metrics for this codec have been flushed due to the
+         * start of a new subsession.
+         * <p>
+         * This can happen when the codec is reconfigured after stop(), or
+         * mid-stream e.g. if the video size changes. When this happens, the
+         * metrics for the previous subsession are flushed, and
+         * {@link MediaCodec#getMetrics} will return the metrics for the
+         * new subsession. This happens just before the {@link Callback#onOutputFormatChanged}
+         * event, so this <b>optional</b> callback is provided to be able to
+         * capture the final metrics for the previous subsession.
+         *
+         * @param codec The MediaCodec object.
+         * @param metrics The flushed metrics for this codec.
+         */
+        @FlaggedApi(FLAG_SUBSESSION_METRICS)
+        public void onMetricsFlushed(
+                @NonNull MediaCodec codec, @NonNull PersistableBundle metrics) {
+            // default implementation ignores this callback.
+        }
     }
 
     private void postEventFromNative(
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index b08a86e..bd65b2e 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import static android.media.codec.Flags.FLAG_IN_PROCESS_SW_AUDIO_CODEC;
+import static android.media.codec.Flags.FLAG_NUM_INPUT_SLOTS;
 import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
 import static android.media.codec.Flags.FLAG_APV_SUPPORT;
 
@@ -1777,6 +1778,17 @@
     public static final String KEY_SECURITY_MODEL = "security-model";
 
     /**
+     * A key describing the number of slots used in the codec. When present in input format,
+     * the associated value indicates the number of input slots. The entry is set by the codec
+     * if configured with (@link MediaCodec#CONFIGURE_FLAG_BLOCK_MODEL), and will be ignored if set
+     * by the application.
+     * <p>
+     * The associated value is an integer.
+     */
+    @FlaggedApi(FLAG_NUM_INPUT_SLOTS)
+    public static final String KEY_NUM_SLOTS = "num-slots";
+
+    /**
      * QpOffsetRect constitutes the metadata required for encoding a region of interest in an
      * image or a video frame. The region of interest is represented by a rectangle. The four
      * integer coordinates of the rectangle are stored in fields left, top, right, bottom.
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3499c43..20108e7 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -1771,10 +1771,12 @@
     }
 
     /**
-     * A class to control media routing session in media route provider. For example,
-     * selecting/deselecting/transferring to routes of a session can be done through this. Instances
-     * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
-     * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called.
+     * Controls a media routing session.
+     *
+     * <p>Routing controllers wrap a {@link RoutingSessionInfo}, taking care of mapping route ids to
+     * {@link MediaRoute2Info} instances. You can still access the underlying session using {@link
+     * #getRoutingSessionInfo()}, but keep in mind it can be changed by other threads. Changes to
+     * the routing session are notified via {@link ControllerCallback}.
      */
     public class RoutingController {
         private final Object mControllerLock = new Object();
@@ -1836,7 +1838,9 @@
         }
 
         /**
-         * @return the unmodifiable list of currently selected routes
+         * Returns the unmodifiable list of currently selected routes
+         *
+         * @see RoutingSessionInfo#getSelectedRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getSelectedRoutes() {
@@ -1848,7 +1852,9 @@
         }
 
         /**
-         * @return the unmodifiable list of selectable routes for the session.
+         * Returns the unmodifiable list of selectable routes for the session.
+         *
+         * @see RoutingSessionInfo#getSelectableRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getSelectableRoutes() {
@@ -1860,7 +1866,9 @@
         }
 
         /**
-         * @return the unmodifiable list of deselectable routes for the session.
+         * Returns the unmodifiable list of deselectable routes for the session.
+         *
+         * @see RoutingSessionInfo#getDeselectableRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getDeselectableRoutes() {
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 83a4dd5..3b8cf3f 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -262,7 +262,8 @@
     }
 
     /**
-     * Gets the provider id of the session.
+     * Gets the provider ID of the session.
+     *
      * @hide
      */
     @Nullable
@@ -271,7 +272,15 @@
     }
 
     /**
-     * Gets the list of IDs of selected routes for the session. It shouldn't be empty.
+     * Gets the list of IDs of selected routes for the session.
+     *
+     * <p>Selected routes are the routes that this session is actively routing media to.
+     *
+     * <p>The behavior of a routing session with multiple selected routes is ultimately defined by
+     * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that
+     * all the selected routes of a routing session are playing the same media in sync.
+     *
+     * @return A non-empty list of selected route ids.
      */
     @NonNull
     public List<String> getSelectedRoutes() {
@@ -280,6 +289,16 @@
 
     /**
      * Gets the list of IDs of selectable routes for the session.
+     *
+     * <p>Selectable routes can be added to a routing session (via {@link
+     * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link
+     * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along
+     * with the other selected routes.
+     *
+     * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring
+     * to a route makes it the sole selected route.
+     *
+     * @return A possibly empty list of selectable route ids.
      */
     @NonNull
     public List<String> getSelectableRoutes() {
@@ -288,6 +307,17 @@
 
     /**
      * Gets the list of IDs of deselectable routes for the session.
+     *
+     * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes},
+     * so that the routing session stops routing to the newly deselected route, but continues on any
+     * remaining selected routes.
+     *
+     * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected
+     * routes}, meaning not all of the selected routes might be deselectable. For example, one of
+     * the selected routes may be a leader device coordinating group playback, which must always
+     * remain selected while the session is active.
+     *
+     * @return A possibly empty list of deselectable route ids.
      */
     @NonNull
     public List<String> getDeselectableRoutes() {
@@ -296,6 +326,24 @@
 
     /**
      * Gets the list of IDs of transferable routes for the session.
+     *
+     * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the
+     * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback
+     * to move from one route to another.
+     *
+     * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because
+     * selecting a route makes it part of the selected routes, while transferring to a route makes
+     * it the selected route. A route can be both transferable and selectable.
+     *
+     * <p>Note that playback may transfer across routes without the target route being in the list
+     * of transferable routes. This can happen by creating a new routing session to the target
+     * route, and releasing the routing session being transferred from. The difference is that a
+     * transfer to a route in the transferable list can happen with no intervention from the app,
+     * with the route provider taking care of the entire operation. A transfer to a route that is
+     * not in the list of transferable routes (by creating a new session) requires the app to move
+     * the playback state from one device to the other.
+     *
+     * @return A possibly empty list of transferable route ids.
      */
     @NonNull
     public List<String> getTransferableRoutes() {
diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java
index c1d73f9..8521d1c 100644
--- a/media/java/android/media/audio/common/AidlConversion.java
+++ b/media/java/android/media/audio/common/AidlConversion.java
@@ -705,6 +705,10 @@
                 aidl.type = AudioDeviceType.OUT_BROADCAST;
                 aidl.connection = AudioDeviceDescription.CONNECTION_BT_LE;
                 break;
+            case AudioSystem.DEVICE_OUT_MULTICHANNEL_GROUP:
+                aidl.type = AudioDeviceType.OUT_MULTICHANNEL_GROUP;
+                aidl.connection = AudioDeviceDescription.CONNECTION_VIRTUAL;
+                break;
             case AudioSystem.DEVICE_IN_BUILTIN_MIC:
                 aidl.type = AudioDeviceType.IN_MICROPHONE;
                 break;
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 90b4aba..7895eb2 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -51,7 +51,7 @@
     is_exported: true
     namespace: "media_tv"
     description: "Enables the following type constant in MediaRoute2Info: LINE_ANALOG, LINE_DIGITAL, AUX_LINE"
-    bug: "301713440"
+    bug: "375691732"
 }
 
 flag {
@@ -79,7 +79,7 @@
 
 flag {
     name: "update_client_profile_priority"
-    namespace: "media"
+    namespace: "media_solutions"
     description : "Feature flag to add updateResourcePriority api to MediaCas"
     bug: "300565729"
 }
@@ -166,3 +166,10 @@
     description: "Allows audio input devices routing and volume control via system settings."
     bug: "355684672"
 }
+
+flag {
+    name: "enable_mirroring_in_media_router_2"
+    namespace: "media_better_together"
+    description: "Enables support for mirroring routes in the MediaRouter2 framework, allowing Output Switcher to offer mirroring routes."
+    bug: "362507305"
+}
diff --git a/media/java/android/media/quality/AmbientBacklightSettings.java b/media/java/android/media/quality/AmbientBacklightSettings.java
index 391eb22..bb782bf 100644
--- a/media/java/android/media/quality/AmbientBacklightSettings.java
+++ b/media/java/android/media/quality/AmbientBacklightSettings.java
@@ -26,6 +26,7 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
+ * Settings for ambient backlight.
  * @hide
  */
 public class AmbientBacklightSettings implements Parcelable {
diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl
index e6c79dd..250d59b 100644
--- a/media/java/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/IMediaQualityManager.aidl
@@ -30,20 +30,22 @@
  */
 interface IMediaQualityManager {
     PictureProfile createPictureProfile(in PictureProfile pp);
-    void updatePictureProfile(in long id, in PictureProfile pp);
-    void removePictureProfile(in long id);
-    PictureProfile getPictureProfileById(in long id);
+    void updatePictureProfile(in String id, in PictureProfile pp);
+    void removePictureProfile(in String id);
+    PictureProfile getPictureProfile(in int type, in String name);
     List<PictureProfile> getPictureProfilesByPackage(in String packageName);
     List<PictureProfile> getAvailablePictureProfiles();
-    List<PictureProfile> getAllPictureProfiles();
+    List<String> getPictureProfilePackageNames();
+    List<String> getPictureProfileAllowList();
+    void setPictureProfileAllowList(in List<String> packages);
 
     SoundProfile createSoundProfile(in SoundProfile pp);
-    void updateSoundProfile(in long id, in SoundProfile pp);
-    void removeSoundProfile(in long id);
-    SoundProfile getSoundProfileById(in long id);
+    void updateSoundProfile(in String id, in SoundProfile pp);
+    void removeSoundProfile(in String id);
+    SoundProfile getSoundProfileById(in String id);
     List<SoundProfile> getSoundProfilesByPackage(in String packageName);
     List<SoundProfile> getAvailableSoundProfiles();
-    List<SoundProfile> getAllSoundProfiles();
+    List<String> getSoundProfilePackageNames();
 
     void registerPictureProfileCallback(in IPictureProfileCallback cb);
     void registerSoundProfileCallback(in ISoundProfileCallback cb);
diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/IPictureProfileCallback.aidl
index 05441cd..34aa2b0 100644
--- a/media/java/android/media/quality/IPictureProfileCallback.aidl
+++ b/media/java/android/media/quality/IPictureProfileCallback.aidl
@@ -17,6 +17,7 @@
 
 package android.media.quality;
 
+import android.media.quality.ParamCapability;
 import android.media.quality.PictureProfile;
 
 /**
@@ -24,7 +25,9 @@
  * @hide
  */
 oneway interface IPictureProfileCallback {
-    void onPictureProfileAdded(in long id, in PictureProfile p);
-    void onPictureProfileUpdated(in long id, in PictureProfile p);
-    void onPictureProfileRemoved(in long id, in PictureProfile p);
+    void onPictureProfileAdded(in String id, in PictureProfile p);
+    void onPictureProfileUpdated(in String id, in PictureProfile p);
+    void onPictureProfileRemoved(in String id, in PictureProfile p);
+    void onParamCapabilitiesChanged(in String id, in List<ParamCapability> caps);
+    void onError(in int err);
 }
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index 38a2025..26d83ac 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -19,6 +19,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemService;
 import android.content.Context;
 import android.media.tv.flags.Flags;
@@ -63,7 +64,7 @@
         mService = service;
         IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() {
             @Override
-            public void onPictureProfileAdded(long profileId, PictureProfile profile) {
+            public void onPictureProfileAdded(String profileId, PictureProfile profile) {
                 synchronized (mLock) {
                     for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
                         // TODO: filter callback record
@@ -72,7 +73,7 @@
                 }
             }
             @Override
-            public void onPictureProfileUpdated(long profileId, PictureProfile profile) {
+            public void onPictureProfileUpdated(String profileId, PictureProfile profile) {
                 synchronized (mLock) {
                     for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
                         // TODO: filter callback record
@@ -81,7 +82,7 @@
                 }
             }
             @Override
-            public void onPictureProfileRemoved(long profileId, PictureProfile profile) {
+            public void onPictureProfileRemoved(String profileId, PictureProfile profile) {
                 synchronized (mLock) {
                     for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
                         // TODO: filter callback record
@@ -89,6 +90,24 @@
                     }
                 }
             }
+            @Override
+            public void onParamCapabilitiesChanged(String profileId, List<ParamCapability> caps) {
+                synchronized (mLock) {
+                    for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
+                        // TODO: filter callback record
+                        record.postParamCapabilitiesChanged(profileId, caps);
+                    }
+                }
+            }
+            @Override
+            public void onError(int err) {
+                synchronized (mLock) {
+                    for (PictureProfileCallbackRecord record : mPpCallbackRecords) {
+                        // TODO: filter callback record
+                        record.postError(err);
+                    }
+                }
+            }
         };
         ISoundProfileCallback spCallback = new ISoundProfileCallback.Stub() {
             @Override
@@ -175,14 +194,17 @@
 
 
     /**
-     * Gets picture profile by given profile ID.
-     * @return the corresponding picture profile if available; {@code null} if the ID doesn't
-     *         exist or the profile is not accessible to the caller.
+     * Gets picture profile by given profile type and name.
+     *
+     * @return the corresponding picture profile if available; {@code null} if the name doesn't
+     *         exist.
      * @hide
      */
-    public PictureProfile getPictureProfileById(long profileId) {
+    @Nullable
+    public PictureProfile getPictureProfile(
+            @PictureProfile.ProfileType int type, @NonNull String name) {
         try {
-            return mService.getPictureProfileById(profileId);
+            return mService.getPictureProfile(type, name);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -190,11 +212,13 @@
 
 
     /**
-     * @SystemApi gets profiles that available to the given package
-     * @hide
+     * Gets profiles that available to the given package.
+     *
+     * @hide @SystemApi
      */
+    @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
-    public List<PictureProfile> getPictureProfilesByPackage(String packageName) {
+    public List<PictureProfile> getPictureProfilesByPackage(@NonNull String packageName) {
         try {
             return mService.getPictureProfilesByPackage(packageName);
         } catch (RemoteException e) {
@@ -215,13 +239,16 @@
     }
 
     /**
-     * @SystemApi all stored picture profiles of all packages
-     * @hide
+     * Gets all package names whose picture profiles are available.
+     *
+     * @see #getPictureProfilesByPackage(String)
+     * @hide @SystemApi
      */
+    @NonNull
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
-    public List<PictureProfile> getAllPictureProfiles() {
+    public List<String> getPictureProfilePackageNames() {
         try {
-            return mService.getAllPictureProfiles();
+            return mService.getPictureProfilePackageNames();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -231,10 +258,12 @@
     /**
      * Creates a picture profile and store it in the system.
      *
-     * @return the stored profile with an assigned profile ID.
+     * @return the stored profile with an assigned profile ID. {@code null} if it's not created
+     * successfully.
      * @hide
      */
-    public PictureProfile createPictureProfile(PictureProfile pp) {
+    @Nullable
+    public PictureProfile createPictureProfile(@NonNull PictureProfile pp) {
         try {
             return mService.createPictureProfile(pp);
         } catch (RemoteException e) {
@@ -247,7 +276,7 @@
      * Updates an existing picture profile and store it in the system.
      * @hide
      */
-    public void updatePictureProfile(long profileId, PictureProfile pp) {
+    public void updatePictureProfile(@NonNull String profileId, @NonNull PictureProfile pp) {
         try {
             mService.updatePictureProfile(profileId, pp);
         } catch (RemoteException e) {
@@ -260,7 +289,7 @@
      * Removes a picture profile from the system.
      * @hide
      */
-    public void removePictureProfile(long profileId) {
+    public void removePictureProfile(@NonNull String profileId) {
         try {
             mService.removePictureProfile(profileId);
         } catch (RemoteException e) {
@@ -307,7 +336,7 @@
      *         exist or the profile is not accessible to the caller.
      * @hide
      */
-    public SoundProfile getSoundProfileById(long profileId) {
+    public SoundProfile getSoundProfileById(String profileId) {
         try {
             return mService.getSoundProfileById(profileId);
         } catch (RemoteException e) {
@@ -346,9 +375,9 @@
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
-    public List<SoundProfile> getAllSoundProfiles() {
+    public List<String> getSoundProfilePackageNames() {
         try {
-            return mService.getAllSoundProfiles();
+            return mService.getSoundProfilePackageNames();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -374,7 +403,7 @@
      * Updates an existing sound profile and store it in the system.
      * @hide
      */
-    public void updateSoundProfile(long profileId, SoundProfile sp) {
+    public void updateSoundProfile(String profileId, SoundProfile sp) {
         try {
             mService.updateSoundProfile(profileId, sp);
         } catch (RemoteException e) {
@@ -387,7 +416,7 @@
      * Removes a sound profile from the system.
      * @hide
      */
-    public void removeSoundProfile(long profileId) {
+    public void removeSoundProfile(String profileId) {
         try {
             mService.removeSoundProfile(profileId);
         } catch (RemoteException e) {
@@ -399,7 +428,8 @@
      * Gets capability information of the given parameters.
      * @hide
      */
-    public List<ParamCapability> getParamCapabilities(List<String> names) {
+    @NonNull
+    public List<ParamCapability> getParamCapabilities(@NonNull List<String> names) {
         try {
             return mService.getParamCapabilities(names);
         } catch (RemoteException e) {
@@ -408,7 +438,38 @@
     }
 
     /**
+     * Gets the allowlist of packages that can create and removed picture profiles
+     *
+     * @see #createPictureProfile(PictureProfile)
+     * @see #removePictureProfile(String)
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
+    @NonNull
+    public List<String> getPictureProfileAllowList() {
+        try {
+            return mService.getPictureProfileAllowList();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sets the allowlist of packages that can create and removed picture profiles
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
+    public void setPictureProfileAllowList(@NonNull List<String> packageNames) {
+        try {
+            mService.setPictureProfileAllowList(packageNames);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Returns {@code true} if media quality HAL is implemented; {@code false} otherwise.
+     * @hide
      */
     public boolean isSupported() {
         try {
@@ -581,7 +642,7 @@
             return mCallback;
         }
 
-        public void postPictureProfileAdded(final long id, PictureProfile profile) {
+        public void postPictureProfileAdded(final String id, PictureProfile profile) {
 
             mExecutor.execute(new Runnable() {
                 @Override
@@ -591,7 +652,7 @@
             });
         }
 
-        public void postPictureProfileUpdated(final long id, PictureProfile profile) {
+        public void postPictureProfileUpdated(final String id, PictureProfile profile) {
             mExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
@@ -600,7 +661,7 @@
             });
         }
 
-        public void postPictureProfileRemoved(final long id, PictureProfile profile) {
+        public void postPictureProfileRemoved(final String id, PictureProfile profile) {
             mExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
@@ -608,6 +669,24 @@
                 }
             });
         }
+
+        public void postParamCapabilitiesChanged(final String id, List<ParamCapability> caps) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onParamCapabilitiesChanged(id, caps);
+                }
+            });
+        }
+
+        public void postError(int error) {
+            mExecutor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mCallback.onError(error);
+                }
+            });
+        }
     }
 
     private static final class SoundProfileCallbackRecord {
@@ -681,24 +760,57 @@
      */
     public abstract static class PictureProfileCallback {
         /**
+         * This is invoked when a picture profile has been added.
+         *
+         * @param profileId the ID of the profile.
+         * @param profile the newly added profile.
          * @hide
          */
-        public void onPictureProfileAdded(long id, PictureProfile profile) {
+        public void onPictureProfileAdded(
+                @NonNull String profileId, @NonNull PictureProfile profile) {
         }
+
         /**
+         * This is invoked when a picture profile has been updated.
+         *
+         * @param profileId the ID of the profile.
+         * @param profile the profile with updated info.
          * @hide
          */
-        public void onPictureProfileUpdated(long id, PictureProfile profile) {
+        public void onPictureProfileUpdated(
+                @NonNull String profileId, @NonNull PictureProfile profile) {
         }
+
         /**
+         * This is invoked when a picture profile has been removed.
+         *
+         * @param profileId the ID of the profile.
+         * @param profile the removed profile.
          * @hide
          */
-        public void onPictureProfileRemoved(long id, PictureProfile profile) {
+        public void onPictureProfileRemoved(
+                @NonNull String profileId, @NonNull PictureProfile profile) {
         }
+
         /**
+         * This is invoked when an issue has occurred.
+         *
+         * @param errorCode the error code
          * @hide
          */
-        public void onError(int errorCode) {
+        public void onError(@PictureProfile.ErrorCode int errorCode) {
+        }
+
+        /**
+         * This is invoked when parameter capabilities has been changed due to status changes of the
+         * content.
+         *
+         * @param profileId the ID of the profile used by the media content.
+         * @param updatedCaps the updated capabilities.
+         * @hide
+         */
+        public void onParamCapabilitiesChanged(
+                @NonNull String profileId, @NonNull List<ParamCapability> updatedCaps) {
         }
     }
 
diff --git a/media/java/android/media/quality/ParamCapability.java b/media/java/android/media/quality/ParamCapability.java
index 70e8592..0b698a9 100644
--- a/media/java/android/media/quality/ParamCapability.java
+++ b/media/java/android/media/quality/ParamCapability.java
@@ -34,7 +34,7 @@
  * @hide
  */
 @FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW)
-public class ParamCapability implements Parcelable {
+public final class ParamCapability implements Parcelable {
 
     /** @hide */
     @IntDef(flag = true, prefix = { "TYPE_" }, value = {
@@ -104,6 +104,7 @@
     @NonNull
     private final Bundle mCaps;
 
+    /** @hide */
     protected ParamCapability(Parcel in) {
         mName = in.readString();
         mIsSupported = in.readBoolean();
@@ -112,7 +113,7 @@
     }
 
     @Override
-    public void writeToParcel(Parcel dest, int flags) {
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString(mName);
         dest.writeBoolean(mIsSupported);
         dest.writeInt(mType);
@@ -124,6 +125,7 @@
         return 0;
     }
 
+    @NonNull
     public static final Creator<ParamCapability> CREATOR = new Creator<ParamCapability>() {
         @Override
         public ParamCapability createFromParcel(Parcel in) {
diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java
index 8fb5712..2be47dd 100644
--- a/media/java/android/media/quality/PictureProfile.java
+++ b/media/java/android/media/quality/PictureProfile.java
@@ -71,6 +71,53 @@
      */
     public static final int TYPE_APPLICATION = 2;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, prefix = "ERROR_", value = {
+            ERROR_UNKNOWN,
+            ERROR_NO_PERMISSION,
+            ERROR_DUPLICATE,
+            ERROR_INVALID_ARGUMENT,
+            ERROR_NOT_ALLOWLISTED
+    })
+    public @interface ErrorCode {}
+
+    /**
+     * Error code for unknown errors.
+     * @hide
+     */
+    public static final int ERROR_UNKNOWN = 0;
+
+    /**
+     * Error code for missing necessary permission to handle the profiles.
+     * @hide
+     */
+    public static final int ERROR_NO_PERMISSION = 1;
+
+    /**
+     * Error code for creating a profile with existing profile type and name.
+     *
+     * @see #getProfileType()
+     * @see #getName()
+     * @hide
+     */
+    public static final int ERROR_DUPLICATE = 2;
+
+    /**
+     * Error code for invalid argument.
+     * @hide
+     */
+    public static final int ERROR_INVALID_ARGUMENT = 3;
+
+    /**
+     * Error code for the case when an operation requires an allowlist but the caller is not in the
+     * list.
+     *
+     * @see MediaQualityManager#getPictureProfileAllowList()
+     * @hide
+     */
+    public static final int ERROR_NOT_ALLOWLISTED = 4;
+
 
     private PictureProfile(@NonNull Parcel in) {
         mId = in.readString();
diff --git a/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java b/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java
index 0957390..d9a1221 100644
--- a/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java
+++ b/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -504,6 +505,27 @@
         assertEquals(AudioDeviceType.OUT_DEVICE, port.ext.getDevice().device.type.type);
     }
 
+    @Test
+    public void testAudioDeviceDescriptionConversion() {
+        for (int nativeDeviceType : AudioSystem.DEVICE_OUT_ALL_SET) {
+            assertNotEquals(
+                    AidlConversion.api2aidl_NativeType_AudioDeviceDescription(nativeDeviceType)
+                            .type,
+                    AudioDeviceType.NONE);
+        }
+
+        for (int nativeDeviceType : AudioSystem.DEVICE_IN_ALL_SET) {
+            if (nativeDeviceType == AudioSystem.DEVICE_IN_COMMUNICATION
+                    || nativeDeviceType == AudioSystem.DEVICE_IN_AMBIENT) {
+                continue;
+            }
+            assertNotEquals(
+                    AidlConversion.api2aidl_NativeType_AudioDeviceDescription(nativeDeviceType)
+                            .type,
+                    AudioDeviceType.NONE);
+        }
+    }
+
     private static AudioFormatDescription createPcm16FormatAidl() {
         final AudioFormatDescription aidl = new AudioFormatDescription();
         aidl.type = AudioFormatType.PCM;
diff --git a/native/android/Android.bp b/native/android/Android.bp
index 3eb99c3..da29c49 100644
--- a/native/android/Android.bp
+++ b/native/android/Android.bp
@@ -55,6 +55,7 @@
         "surface_control_input_receiver.cpp",
         "choreographer.cpp",
         "configuration.cpp",
+        "dynamic_instrumentation_manager.cpp",
         "hardware_buffer_jni.cpp",
         "input.cpp",
         "input_transfer_token.cpp",
@@ -100,6 +101,7 @@
         "android.hardware.configstore@1.0",
         "android.hardware.configstore-utils",
         "android.os.flags-aconfig-cc",
+        "dynamic_instrumentation_manager_aidl-cpp",
         "libnativedisplay",
         "libfmq",
     ],
diff --git a/native/android/dynamic_instrumentation_manager.cpp b/native/android/dynamic_instrumentation_manager.cpp
new file mode 100644
index 0000000..d9bacb1
--- /dev/null
+++ b/native/android/dynamic_instrumentation_manager.cpp
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "ADynamicInstrumentationManager"
+#include <android/dynamic_instrumentation_manager.h>
+#include <android/os/instrumentation/ExecutableMethodFileOffsets.h>
+#include <android/os/instrumentation/IDynamicInstrumentationManager.h>
+#include <android/os/instrumentation/MethodDescriptor.h>
+#include <android/os/instrumentation/TargetProcess.h>
+#include <binder/Binder.h>
+#include <binder/IServiceManager.h>
+#include <utils/Log.h>
+
+#include <mutex>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace android::dynamicinstrumentationmanager {
+
+// Global instance of IDynamicInstrumentationManager, service is obtained only on first use.
+static std::mutex mLock;
+static sp<os::instrumentation::IDynamicInstrumentationManager> mService;
+
+sp<os::instrumentation::IDynamicInstrumentationManager> getService() {
+    std::lock_guard<std::mutex> scoped_lock(mLock);
+    if (mService == nullptr || !IInterface::asBinder(mService)->isBinderAlive()) {
+        sp<IBinder> binder =
+                defaultServiceManager()->waitForService(String16("dynamic_instrumentation"));
+        mService = interface_cast<os::instrumentation::IDynamicInstrumentationManager>(binder);
+    }
+    return mService;
+}
+
+} // namespace android::dynamicinstrumentationmanager
+
+using namespace android;
+using namespace dynamicinstrumentationmanager;
+
+struct ADynamicInstrumentationManager_TargetProcess {
+    uid_t uid;
+    uid_t pid;
+    std::string processName;
+
+    ADynamicInstrumentationManager_TargetProcess(uid_t uid, pid_t pid, const char* processName)
+          : uid(uid), pid(pid), processName(processName) {}
+};
+
+ADynamicInstrumentationManager_TargetProcess* ADynamicInstrumentationManager_TargetProcess_create(
+        uid_t uid, pid_t pid, const char* processName) {
+    return new ADynamicInstrumentationManager_TargetProcess(uid, pid, processName);
+}
+
+void ADynamicInstrumentationManager_TargetProcess_destroy(
+        ADynamicInstrumentationManager_TargetProcess* instance) {
+    delete instance;
+}
+
+struct ADynamicInstrumentationManager_MethodDescriptor {
+    std::string fqcn;
+    std::string methodName;
+    std::vector<std::string> fqParameters;
+
+    ADynamicInstrumentationManager_MethodDescriptor(const char* fqcn, const char* methodName,
+                                                    const char* fullyQualifiedParameters[],
+                                                    size_t numParameters)
+          : fqcn(fqcn), methodName(methodName) {
+        std::vector<std::string> fqParameters;
+        fqParameters.reserve(numParameters);
+        std::copy_n(fullyQualifiedParameters, numParameters, std::back_inserter(fqParameters));
+        this->fqParameters = std::move(fqParameters);
+    }
+};
+
+ADynamicInstrumentationManager_MethodDescriptor*
+ADynamicInstrumentationManager_MethodDescriptor_create(const char* fullyQualifiedClassName,
+                                                       const char* methodName,
+                                                       const char* fullyQualifiedParameters[],
+                                                       size_t numParameters) {
+    return new ADynamicInstrumentationManager_MethodDescriptor(fullyQualifiedClassName, methodName,
+                                                               fullyQualifiedParameters,
+                                                               numParameters);
+}
+
+void ADynamicInstrumentationManager_MethodDescriptor_destroy(
+        ADynamicInstrumentationManager_MethodDescriptor* instance) {
+    delete instance;
+}
+
+struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets {
+    std::string containerPath;
+    uint64_t containerOffset;
+    uint64_t methodOffset;
+};
+
+ADynamicInstrumentationManager_ExecutableMethodFileOffsets*
+ADynamicInstrumentationManager_ExecutableMethodFileOffsets_create() {
+    return new ADynamicInstrumentationManager_ExecutableMethodFileOffsets();
+}
+
+const char* ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) {
+    return instance->containerPath.c_str();
+}
+
+uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) {
+    return instance->containerOffset;
+}
+
+uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) {
+    return instance->methodOffset;
+}
+
+void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* instance) {
+    delete instance;
+}
+
+int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets(
+        const ADynamicInstrumentationManager_TargetProcess* targetProcess,
+        const ADynamicInstrumentationManager_MethodDescriptor* methodDescriptor,
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets** out) {
+    android::os::instrumentation::TargetProcess targetProcessParcel;
+    targetProcessParcel.uid = targetProcess->uid;
+    targetProcessParcel.pid = targetProcess->pid;
+    targetProcessParcel.processName = targetProcess->processName;
+
+    android::os::instrumentation::MethodDescriptor methodDescriptorParcel;
+    methodDescriptorParcel.fullyQualifiedClassName = methodDescriptor->fqcn;
+    methodDescriptorParcel.methodName = methodDescriptor->methodName;
+    methodDescriptorParcel.fullyQualifiedParameters = methodDescriptor->fqParameters;
+
+    sp<os::instrumentation::IDynamicInstrumentationManager> service = getService();
+    if (service == nullptr) {
+        return INVALID_OPERATION;
+    }
+
+    std::optional<android::os::instrumentation::ExecutableMethodFileOffsets> offsets;
+    binder_status_t result =
+            service->getExecutableMethodFileOffsets(targetProcessParcel, methodDescriptorParcel,
+                                                    &offsets)
+                    .exceptionCode();
+    if (result != OK) {
+        return result;
+    }
+
+    if (offsets != std::nullopt) {
+        auto* value = new ADynamicInstrumentationManager_ExecutableMethodFileOffsets();
+        value->containerPath = offsets->containerPath;
+        value->containerOffset = offsets->containerOffset;
+        value->methodOffset = offsets->methodOffset;
+        *out = value;
+    } else {
+        *out = nullptr;
+    }
+
+    return result;
+}
\ No newline at end of file
diff --git a/native/android/include_platform/android/dynamic_instrumentation_manager.h b/native/android/include_platform/android/dynamic_instrumentation_manager.h
new file mode 100644
index 0000000..6c46288
--- /dev/null
+++ b/native/android/include_platform/android/dynamic_instrumentation_manager.h
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+
+#ifndef __ADYNAMICINSTRUMENTATIONMANAGER_H__
+#define __ADYNAMICINSTRUMENTATIONMANAGER_H__
+
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+__BEGIN_DECLS
+
+struct ADynamicInstrumentationManager_MethodDescriptor;
+typedef struct ADynamicInstrumentationManager_MethodDescriptor
+        ADynamicInstrumentationManager_MethodDescriptor;
+
+struct ADynamicInstrumentationManager_TargetProcess;
+typedef struct ADynamicInstrumentationManager_TargetProcess
+        ADynamicInstrumentationManager_TargetProcess;
+
+struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets;
+typedef struct ADynamicInstrumentationManager_ExecutableMethodFileOffsets
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets;
+
+/**
+ * Initializes an ADynamicInstrumentationManager_TargetProcess. Caller must clean up when they are
+ * done with ADynamicInstrumentationManager_TargetProcess_destroy.
+ *
+ * @param uid of targeted process.
+ * @param pid of targeted process.
+ * @param processName to disambiguate from corner cases that may arise from pid reuse.
+ */
+ADynamicInstrumentationManager_TargetProcess* _Nonnull
+        ADynamicInstrumentationManager_TargetProcess_create(
+                uid_t uid, pid_t pid, const char* _Nonnull processName) __INTRODUCED_IN(36);
+/**
+ * Clean up an ADynamicInstrumentationManager_TargetProcess.
+ *
+ * @param instance returned from ADynamicInstrumentationManager_TargetProcess_create.
+ */
+void ADynamicInstrumentationManager_TargetProcess_destroy(
+        ADynamicInstrumentationManager_TargetProcess* _Nonnull instance) __INTRODUCED_IN(36);
+
+/**
+ * Initializes an ADynamicInstrumentationManager_MethodDescriptor. Caller must clean up when they
+ * are done with ADynamicInstrumentationManager_MethodDescriptor_Destroy.
+ *
+ * @param fullyQualifiedClassName fqcn of class containing the method.
+ * @param methodName
+ * @param fullyQualifiedParameters fqcn of parameters of the method's signature, or e.g. "int" for
+ *                                 primitives.
+ * @param numParameters length of `fullyQualifiedParameters` array.
+ */
+ADynamicInstrumentationManager_MethodDescriptor* _Nonnull
+        ADynamicInstrumentationManager_MethodDescriptor_create(
+                const char* _Nonnull fullyQualifiedClassName, const char* _Nonnull methodName,
+                const char* _Nonnull fullyQualifiedParameters[_Nonnull], size_t numParameters)
+                __INTRODUCED_IN(36);
+/**
+ * Clean up an ADynamicInstrumentationManager_MethodDescriptor.
+ *
+ * @param instance returned from ADynamicInstrumentationManager_MethodDescriptor_create.
+ */
+void ADynamicInstrumentationManager_MethodDescriptor_destroy(
+        ADynamicInstrumentationManager_MethodDescriptor* _Nonnull instance) __INTRODUCED_IN(36);
+
+/**
+ * Get the containerPath calculated by
+ * ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @return The OS path of the containing file.
+ */
+const char* _Nullable ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance)
+        __INTRODUCED_IN(36);
+/**
+ * Get the containerOffset calculated by
+ * ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @return The offset of the containing file within the process' memory.
+ */
+uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance)
+        __INTRODUCED_IN(36);
+/**
+ * Get the methodOffset calculated by ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @param instance created with ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ * @return The offset of the method within the containing file.
+ */
+uint64_t ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance)
+        __INTRODUCED_IN(36);
+/**
+ * Clean up an ADynamicInstrumentationManager_ExecutableMethodFileOffsets.
+ *
+ * @param instance returned from ADynamicInstrumentationManager_getExecutableMethodFileOffsets.
+ */
+void ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy(
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull instance)
+        __INTRODUCED_IN(36);
+/**
+ * Provides ART metadata about the described java method within the target process.
+ *
+ * @param targetProcess describes for which process the data is requested.
+ * @param methodDescriptor describes the targeted method.
+ * @param out will be populated with the data if successful. A nullptr combined
+ *        with an OK status means that the program method is defined, but the offset
+ *        info was unavailable because it is not AOT compiled.
+ * @return status indicating success or failure. The values correspond to the `binder_exception_t`
+ *         enum values from <android/binder_status.h>.
+ */
+int32_t ADynamicInstrumentationManager_getExecutableMethodFileOffsets(
+        const ADynamicInstrumentationManager_TargetProcess* _Nonnull targetProcess,
+        const ADynamicInstrumentationManager_MethodDescriptor* _Nonnull methodDescriptor,
+        ADynamicInstrumentationManager_ExecutableMethodFileOffsets* _Nonnull* _Nullable out)
+        __INTRODUCED_IN(36);
+
+__END_DECLS
+
+#endif // __ADYNAMICINSTRUMENTATIONMANAGER_H__
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index b025cb8..a046057 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -4,6 +4,15 @@
     AActivityManager_removeUidImportanceListener; # systemapi introduced=31
     AActivityManager_isUidActive; # systemapi introduced=31
     AActivityManager_getUidImportance; # systemapi introduced=31
+    ADynamicInstrumentationManager_TargetProcess_create; # systemapi
+    ADynamicInstrumentationManager_TargetProcess_destroy; # systemapi
+    ADynamicInstrumentationManager_MethodDescriptor_create; # systemapi
+    ADynamicInstrumentationManager_MethodDescriptor_destroy; # systemapi
+    ADynamicInstrumentationManager_getExecutableMethodFileOffsets; # systemapi
+    ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerPath; # systemapi
+    ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getContainerOffset; # systemapi
+    ADynamicInstrumentationManager_ExecutableMethodFileOffsets_getMethodOffset; # systemapi
+    ADynamicInstrumentationManager_ExecutableMethodFileOffsets_destroy; # systemapi
     AAssetDir_close;
     AAssetDir_getNextFileName;
     AAssetDir_rewind;
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 675c8f8..a23845f 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -100,6 +100,7 @@
     method public void onHceEventReceived(int);
     method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String);
     method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String);
+    method public void onLogEventNotified(@NonNull android.nfc.OemLogItems);
     method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onReaderOptionChanged(boolean);
@@ -115,6 +116,27 @@
     method public int getNfceeId();
   }
 
+  @FlaggedApi("android.nfc.nfc_oem_extension") public final class OemLogItems implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getCallingPid();
+    method @Nullable public byte[] getCommandApdu();
+    method public int getEvent();
+    method @Nullable public byte[] getResponseApdu();
+    method @Nullable public java.time.Instant getRfFieldEventTimeMillis();
+    method @Nullable public android.nfc.Tag getTag();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.nfc.OemLogItems> CREATOR;
+    field public static final int EVENT_DISABLE = 2; // 0x2
+    field public static final int EVENT_ENABLE = 1; // 0x1
+    field public static final int EVENT_UNSET = 0; // 0x0
+    field public static final int LOG_ACTION_HCE_DATA = 516; // 0x204
+    field public static final int LOG_ACTION_NFC_TOGGLE = 513; // 0x201
+    field public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 1; // 0x1
+    field public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 518; // 0x206
+    field public static final int LOG_ACTION_TAG_DETECTED = 3; // 0x3
+  }
+
   @FlaggedApi("android.nfc.nfc_oem_extension") public class RoutingStatus {
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultIsoDepRoute();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultOffHostRoute();
diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
index 7f1fd15..b102e87 100644
--- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
+++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
@@ -18,6 +18,7 @@
 import android.content.ComponentName;
 import android.nfc.cardemulation.ApduServiceInfo;
 import android.nfc.NdefMessage;
+import android.nfc.OemLogItems;
 import android.nfc.Tag;
 import android.os.ResultReceiver;
 
@@ -51,4 +52,5 @@
    void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent);
    void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category);
    void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category);
+   void onLogEventNotified(in OemLogItems item);
 }
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 1bfe714..abd99bc 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -392,6 +392,12 @@
          * @param category the category of the service
          */
         void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);
+
+        /**
+         * Callback when OEM specified log event are notified.
+         * @param item the log items that contains log information of NFC event.
+         */
+        void onLogEventNotified(@NonNull OemLogItems item);
     }
 
 
@@ -900,6 +906,12 @@
                     handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
         }
 
+        @Override
+        public void onLogEventNotified(OemLogItems item) throws RemoteException  {
+            mCallbackMap.forEach((cb, ex) ->
+                    handleVoidCallback(item, cb::onLogEventNotified, ex));
+        }
+
         private <T> void handleVoidCallback(
                 T input, Consumer<T> callbackMethod, Executor executor) {
             synchronized (mLock) {
diff --git a/nfc/java/android/nfc/OemLogItems.aidl b/nfc/java/android/nfc/OemLogItems.aidl
new file mode 100644
index 0000000..3bcb445
--- /dev/null
+++ b/nfc/java/android/nfc/OemLogItems.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.nfc;
+
+parcelable OemLogItems;
\ No newline at end of file
diff --git a/nfc/java/android/nfc/OemLogItems.java b/nfc/java/android/nfc/OemLogItems.java
new file mode 100644
index 0000000..6671941
--- /dev/null
+++ b/nfc/java/android/nfc/OemLogItems.java
@@ -0,0 +1,325 @@
+/*

+ * Copyright 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.nfc;

+

+import android.annotation.FlaggedApi;

+import android.annotation.IntDef;

+import android.annotation.NonNull;

+import android.annotation.Nullable;

+import android.annotation.SystemApi;

+import android.os.Parcel;

+import android.os.Parcelable;

+

+import java.lang.annotation.Retention;

+import java.lang.annotation.RetentionPolicy;

+import java.time.Instant;

+

+/**

+ * A log class for OEMs to get log information of NFC events.

+ * @hide

+ */

+@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)

+@SystemApi

+public final class OemLogItems implements Parcelable {

+    /**

+     * Used when RF field state is changed.

+     */

+    public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 0X01;

+    /**

+     * Used when NFC is toggled. Event should be set to {@link LogEvent#EVENT_ENABLE} or

+     * {@link LogEvent#EVENT_DISABLE} if this action is used.

+     */

+    public static final int LOG_ACTION_NFC_TOGGLE = 0x0201;

+    /**

+     * Used when sending host routing status.

+     */

+    public static final int LOG_ACTION_HCE_DATA = 0x0204;

+    /**

+     * Used when screen state is changed.

+     */

+    public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 0x0206;

+    /**

+     * Used when tag is detected.

+     */

+    public static final int LOG_ACTION_TAG_DETECTED = 0x03;

+

+    /**

+     * @hide

+     */

+    @IntDef(prefix = { "LOG_ACTION_" }, value = {

+            LOG_ACTION_RF_FIELD_STATE_CHANGED,

+            LOG_ACTION_NFC_TOGGLE,

+            LOG_ACTION_HCE_DATA,

+            LOG_ACTION_SCREEN_STATE_CHANGED,

+            LOG_ACTION_TAG_DETECTED,

+    })

+    @Retention(RetentionPolicy.SOURCE)

+    public @interface LogAction {}

+

+    /**

+     * Represents the event is not set.

+     */

+    public static final int EVENT_UNSET = 0;

+    /**

+     * Represents nfc enable is called.

+     */

+    public static final int EVENT_ENABLE = 1;

+    /**

+     * Represents nfc disable is called.

+     */

+    public static final int EVENT_DISABLE = 2;

+    /** @hide */

+    @IntDef(prefix = { "EVENT_" }, value = {

+            EVENT_UNSET,

+            EVENT_ENABLE,

+            EVENT_DISABLE,

+    })

+    @Retention(RetentionPolicy.SOURCE)

+    public @interface LogEvent {}

+    private int mAction;

+    private int mEvent;

+    private int mCallingPid;

+    private byte[] mCommandApdus;

+    private byte[] mResponseApdus;

+    private Instant mRfFieldOnTime;

+    private Tag mTag;

+

+    /** @hide */

+    public OemLogItems(@LogAction int action, @LogEvent int event, int callingPid,

+            byte[] commandApdus, byte[] responseApdus, Instant rfFieldOnTime,

+            Tag tag) {

+        mAction = action;

+        mEvent = event;

+        mTag = tag;

+        mCallingPid = callingPid;

+        mCommandApdus = commandApdus;

+        mResponseApdus = responseApdus;

+        mRfFieldOnTime = rfFieldOnTime;

+    }

+

+    /**

+     * Describe the kinds of special objects contained in this Parcelable

+     * instance's marshaled representation. For example, if the object will

+     * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},

+     * the return value of this method must include the

+     * {@link #CONTENTS_FILE_DESCRIPTOR} bit.

+     *

+     * @return a bitmask indicating the set of special object types marshaled

+     * by this Parcelable object instance.

+     */

+    @Override

+    public int describeContents() {

+        return 0;

+    }

+

+    /**

+     * Flatten this object in to a Parcel.

+     *

+     * @param dest  The Parcel in which the object should be written.

+     * @param flags Additional flags about how the object should be written.

+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.

+     */

+    @Override

+    public void writeToParcel(@NonNull Parcel dest, int flags) {

+        dest.writeInt(mAction);

+        dest.writeInt(mEvent);

+        dest.writeInt(mCallingPid);

+        dest.writeInt(mCommandApdus.length);

+        dest.writeByteArray(mCommandApdus);

+        dest.writeInt(mResponseApdus.length);

+        dest.writeByteArray(mResponseApdus);

+        dest.writeLong(mRfFieldOnTime.getEpochSecond());

+        dest.writeInt(mRfFieldOnTime.getNano());

+        dest.writeParcelable(mTag, 0);

+    }

+

+    /** @hide */

+    public static class Builder {

+        private final OemLogItems mItem;

+

+        public Builder(@LogAction int type) {

+            mItem = new OemLogItems(type, EVENT_UNSET, 0, new byte[0], new byte[0], null, null);

+        }

+

+        /** Setter of the log action. */

+        public OemLogItems.Builder setAction(@LogAction int action) {

+            mItem.mAction = action;

+            return this;

+        }

+

+        /** Setter of the log calling event. */

+        public OemLogItems.Builder setCallingEvent(@LogEvent int event) {

+            mItem.mEvent = event;

+            return this;

+        }

+

+        /** Setter of the log calling Pid. */

+        public OemLogItems.Builder setCallingPid(int pid) {

+            mItem.mCallingPid = pid;

+            return this;

+        }

+

+        /** Setter of APDU command. */

+        public OemLogItems.Builder setApduCommand(byte[] apdus) {

+            mItem.mCommandApdus = apdus;

+            return this;

+        }

+

+        /** Setter of RF field on time. */

+        public OemLogItems.Builder setRfFieldOnTime(Instant time) {

+            mItem.mRfFieldOnTime = time;

+            return this;

+        }

+

+        /** Setter of APDU response. */

+        public OemLogItems.Builder setApduResponse(byte[] apdus) {

+            mItem.mResponseApdus = apdus;

+            return this;

+        }

+

+        /** Setter of dispatched tag. */

+        public OemLogItems.Builder setTag(Tag tag) {

+            mItem.mTag = tag;

+            return this;

+        }

+

+        /** Builds an {@link OemLogItems} instance. */

+        public OemLogItems build() {

+            return mItem;

+        }

+    }

+

+    /**

+     * Gets the action of this log.

+     * @return one of {@link LogAction}

+     */

+    @LogAction

+    public int getAction() {

+        return mAction;

+    }

+

+    /**

+     * Gets the event of this log. This will be set to {@link LogEvent#EVENT_ENABLE} or

+     * {@link LogEvent#EVENT_DISABLE} only when action is set to

+     * {@link LogAction#LOG_ACTION_NFC_TOGGLE}

+     * @return one of {@link LogEvent}

+     */

+    @LogEvent

+    public int getEvent() {

+        return mEvent;

+    }

+

+    /**

+     * Gets the calling Pid of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_NFC_TOGGLE}

+     * @return calling Pid

+     */

+    public int getCallingPid() {

+        return mCallingPid;

+    }

+

+    /**

+     * Gets the command APDUs of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_HCE_DATA}

+     * @return a byte array of command APDUs with the same format as

+     * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}

+     */

+    @Nullable

+    public byte[] getCommandApdu() {

+        return mCommandApdus;

+    }

+

+    /**

+     * Gets the response APDUs of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_HCE_DATA}

+     * @return a byte array of response APDUs with the same format as

+     * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}

+     */

+    @Nullable

+    public byte[] getResponseApdu() {

+        return mResponseApdus;

+    }

+

+    /**

+     * Gets the RF field event time in this log in millisecond. This field will be set only when

+     * action is set to {@link LogAction#LOG_ACTION_RF_FIELD_STATE_CHANGED}

+     * @return an {@link Instant} of RF field event time.

+     */

+    @Nullable

+    public Instant getRfFieldEventTimeMillis() {

+        return mRfFieldOnTime;

+    }

+

+    /**

+     * Gets the tag of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_TAG_DETECTED}

+     * @return a detected {@link Tag} in {@link #LOG_ACTION_TAG_DETECTED} case. Return

+     * null otherwise.

+     */

+    @Nullable

+    public Tag getTag() {

+        return mTag;

+    }

+

+    private String byteToHex(byte[] bytes) {

+        char[] HexArray = "0123456789ABCDEF".toCharArray();

+        char[] hexChars = new char[bytes.length * 2];

+        for (int j = 0; j < bytes.length; j++) {

+            int v = bytes[j] & 0xFF;

+            hexChars[j * 2] = HexArray[v >>> 4];

+            hexChars[j * 2 + 1] = HexArray[v & 0x0F];

+        }

+        return new String(hexChars);

+    }

+

+    @Override

+    public String toString() {

+        return "[mCommandApdus: "

+                + ((mCommandApdus != null) ? byteToHex(mCommandApdus) : "null")

+                + "[mResponseApdus: "

+                + ((mResponseApdus != null) ? byteToHex(mResponseApdus) : "null")

+                + ", mCallingApi= " + mEvent

+                + ", mAction= " + mAction

+                + ", mCallingPId = " + mCallingPid

+                + ", mRfFieldOnTime= " + mRfFieldOnTime;

+    }

+    private OemLogItems(Parcel in) {

+        this.mAction = in.readInt();

+        this.mEvent = in.readInt();

+        this.mCallingPid = in.readInt();

+        this.mCommandApdus = new byte[in.readInt()];

+        in.readByteArray(this.mCommandApdus);

+        this.mResponseApdus = new byte[in.readInt()];

+        in.readByteArray(this.mResponseApdus);

+        this.mRfFieldOnTime = Instant.ofEpochSecond(in.readLong(), in.readInt());

+        this.mTag = in.readParcelable(Tag.class.getClassLoader(), Tag.class);

+    }

+

+    public static final @NonNull Parcelable.Creator<OemLogItems> CREATOR =

+            new Parcelable.Creator<OemLogItems>() {

+                @Override

+                public OemLogItems createFromParcel(Parcel in) {

+                    return new OemLogItems(in);

+                }

+

+                @Override

+                public OemLogItems[] newArray(int size) {

+                    return new OemLogItems[size];

+                }

+            };

+

+}

diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index e141867..b2dcb7f 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -61,7 +61,7 @@
         "SettingsLibUtils",
         "SettingsLibZeroStatePreference",
         "settingslib_media_flags_lib",
-        "settingslib_flags_lib",
+        "aconfig_settingslib_flags_java_lib",
     ],
 
     plugins: ["androidx.room_room-compiler-plugin"],
@@ -107,20 +107,6 @@
     aconfig_declarations: "settingslib_media_flags",
 }
 
-aconfig_declarations {
-    name: "settingslib_flags",
-    package: "com.android.settingslib.flags",
-    container: "system",
-    srcs: [
-        "aconfig/settingslib.aconfig",
-    ],
-}
-
-java_aconfig_library {
-    name: "settingslib_flags_lib",
-    aconfig_declarations: "settingslib_flags",
-}
-
 soong_config_module_type {
     name: "avatar_picker_java_defaults",
     module_type: "java_defaults",
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
index 3b52df7..c3f6eb7 100644
--- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
+++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppPreference.java
@@ -30,21 +30,28 @@
 
     public AppPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppPreference(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppPreference(Context context) {
         super(context);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
+    }
+
+    private void init(Context context) {
+        int resId = SettingsThemeHelper.isExpressiveTheme(context)
+                ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference
+                : R.layout.preference_app;
+        setLayoutResource(resId);
     }
 }
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
index ecd500e..3dcdfba 100644
--- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
+++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
@@ -32,22 +32,29 @@
     public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppSwitchPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
     }
 
     public AppSwitchPreference(Context context) {
         super(context);
-        setLayoutResource(R.layout.preference_app);
+        init(context);
+    }
+
+    private void init(Context context) {
+        int resId = SettingsThemeHelper.isExpressiveTheme(context)
+                ? com.android.settingslib.widget.theme.R.layout.settingslib_expressive_preference
+                : R.layout.preference_app;
+        setLayoutResource(resId);
     }
 
     @Override
diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
index 979ff96..993555e 100644
--- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
+++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
@@ -37,7 +37,7 @@
 /**
  * A preference handled a button
  */
-public class ButtonPreference extends Preference {
+public class ButtonPreference extends Preference implements GroupSectionDividerMixin {
 
     enum ButtonStyle {
         FILLED_NORMAL(0, 0, R.layout.settingslib_expressive_button_filled),
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
index 843d2aa..cd03dd7 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
@@ -202,6 +202,12 @@
             entry.value.execute { observer.onKeyChanged(key, reason) }
         }
     }
+
+    fun hasAnyObserver(): Boolean {
+        synchronized(observers) { if (observers.isNotEmpty()) return true }
+        synchronized(keyedObservers) { if (keyedObservers.isNotEmpty()) return true }
+        return false
+    }
 }
 
 /** [KeyedObservable] with no-op implementations for all interfaces. */
diff --git a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
index d60290e..37f4754 100644
--- a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
+++ b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java
@@ -43,7 +43,7 @@
  * A custom preference acting as "footer" of a page. It has a field for icon and text. It is added
  * to screen as the last preference.
  */
-public class FooterPreference extends Preference {
+public class FooterPreference extends Preference implements GroupSectionDividerMixin {
     private static final String TAG = "FooterPreference";
 
     public static final String KEY_FOOTER = "footer_preference";
diff --git a/packages/SettingsLib/IntroPreference/Android.bp b/packages/SettingsLib/IntroPreference/Android.bp
index 155db18..8f9fb7a 100644
--- a/packages/SettingsLib/IntroPreference/Android.bp
+++ b/packages/SettingsLib/IntroPreference/Android.bp
@@ -29,5 +29,6 @@
     min_sdk_version: "21",
     apex_available: [
         "//apex_available:platform",
+        "com.android.healthfitness",
     ],
 }
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
index 94d373b..bde4217 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -136,6 +136,18 @@
         for (it in children) action(it)
     }
 
+    /** Traversals preference hierarchy recursively and applies given action. */
+    fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) {
+        action(this)
+        for (child in children) {
+            if (child is PreferenceHierarchy) {
+                child.forEachRecursively(action)
+            } else {
+                action(child)
+            }
+        }
+    }
+
     /** Traversals preference hierarchy and applies given action. */
     suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
         for (it in children) action(it)
@@ -157,18 +169,7 @@
 
     /** Returns all the [PreferenceHierarchyNode]s appear in the hierarchy. */
     fun getAllPreferences(): List<PreferenceHierarchyNode> =
-        mutableListOf<PreferenceHierarchyNode>().also { getAllPreferences(it) }
-
-    private fun getAllPreferences(result: MutableList<PreferenceHierarchyNode>) {
-        result.add(this)
-        for (child in children) {
-            if (child is PreferenceHierarchy) {
-                child.getAllPreferences(result)
-            } else {
-                result.add(child)
-            }
-        }
-    }
+        mutableListOf<PreferenceHierarchyNode>().apply { forEachRecursively { add(it) } }
 }
 
 /**
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
index 41a626f..991d5b7 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -32,7 +32,7 @@
 open class PreferenceFragment :
     SettingsBasePreferenceFragment(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
 
-    private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+    protected var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         preferenceScreen = createPreferenceScreen()
@@ -129,7 +129,9 @@
     }
 
     protected fun getPreferenceKeysInHierarchy(): Set<String> =
-        preferenceScreenBindingHelper?.getPreferences()?.map { it.metadata.key }?.toSet() ?: setOf()
+        preferenceScreenBindingHelper?.let {
+            mutableSetOf<String>().apply { it.forEachRecursively { add(it.metadata.key) } }
+        } ?: setOf()
 
     companion object {
         private const val TAG = "PreferenceFragment"
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 022fb1d..fbe8927 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -143,7 +143,8 @@
         }
     }
 
-    fun getPreferences() = preferenceHierarchy.getAllPreferences()
+    fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) =
+        preferenceHierarchy.forEachRecursively(action)
 
     fun onCreate() {
         for (preference in lifecycleAwarePreferences) {
@@ -191,11 +192,11 @@
 
     companion object {
         /** Preference value is changed. */
-        private const val CHANGE_REASON_VALUE = 0
+        const val CHANGE_REASON_VALUE = 0
         /** Preference state (title/summary, enable state, etc.) is changed. */
-        private const val CHANGE_REASON_STATE = 1
+        const val CHANGE_REASON_STATE = 1
         /** Dependent preference state is changed. */
-        private const val CHANGE_REASON_DEPENDENT = 2
+        const val CHANGE_REASON_DEPENDENT = 2
 
         /** Updates preference screen that has incomplete hierarchy. */
         @JvmStatic
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
index 155ee83..78e27fe 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp
@@ -29,6 +29,7 @@
         "//apex_available:platform",
         "com.android.permission",
         "com.android.mediaprovider",
+        "com.android.healthfitness",
     ],
 }
 
@@ -51,5 +52,6 @@
         "//apex_available:platform",
         "com.android.permission",
         "com.android.mediaprovider",
+        "com.android.healthfitness",
     ],
 }
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml
index ccdf37d..0cd0b3c 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v35/settingslib_expressive_preference_icon_frame.xml
@@ -22,7 +22,7 @@
     android:minWidth="@dimen/settingslib_expressive_space_medium3"
     android:minHeight="@dimen/settingslib_expressive_space_medium3"
     android:gravity="center"
-    android:layout_marginEnd="-8dp"
+    android:layout_marginEnd="-4dp"
     android:filterTouchesWhenObscured="false">
 
     <androidx.preference.internal.PreferenceImageView
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml
index 3c69027..cec8e45 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_preference_expressive.xml
@@ -36,33 +36,34 @@
     <style name="SettingsLibPreference.SwitchPreference" parent="SettingsSwitchPreference.SettingsLib"/>
 
     <style name="SettingsLibPreference.Expressive">
-        <item name="android:layout">@layout/settingslib_expressive_preference</item>
+        <item name="layout">@layout/settingslib_expressive_preference</item>
     </style>
 
     <style name="SettingsLibPreference.Category.Expressive">
     </style>
 
     <style name="SettingsLibPreference.CheckBoxPreference.Expressive">
-        <item name="android:layout">@layout/settingslib_expressive_preference</item>
+        <item name="layout">@layout/settingslib_expressive_preference</item>
     </style>
 
     <style name="SettingsLibPreference.SwitchPreferenceCompat.Expressive">
-        <item name="android:layout">@layout/settingslib_expressive_preference</item>
+        <item name="layout">@layout/settingslib_expressive_preference</item>
         <item name="android:widgetLayout">@layout/settingslib_expressive_preference_switch</item>
     </style>
 
     <style name="SettingsLibPreference.SeekBarPreference.Expressive"/>
 
     <style name="SettingsLibPreference.PreferenceScreen.Expressive">
-        <item name="android:layout">@layout/settingslib_expressive_preference</item>
+        <item name="layout">@layout/settingslib_expressive_preference</item>
     </style>
 
     <style name="SettingsLibPreference.DialogPreference.Expressive">
+        <item name="layout">@layout/settingslib_expressive_preference</item>
     </style>
 
     <style name="SettingsLibPreference.DialogPreference.EditTextPreference.Expressive">
-        <item name="android:layout">@layout/settingslib_expressive_preference</item>
-        <item name="android:dialogLayout">@layout/settingslib_preference_dialog_edittext</item>
+        <item name="layout">@layout/settingslib_expressive_preference</item>
+        <item name="dialogLayout">@layout/settingslib_preference_dialog_edittext</item>
     </style>
 
     <style name="SettingsLibPreference.DropDown.Expressive">
diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
index c1578ef..1f8cfb5 100644
--- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
+++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt
@@ -34,7 +34,7 @@
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
-) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
 
     enum class BannerStatus {
         GENERIC,
diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
index 5be56f8..9764e64 100644
--- a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
+++ b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt
@@ -31,7 +31,7 @@
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
-) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
 
     private var isCollapsable: Boolean = false
     private var minLines: Int = 2
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 0dc772a..ebd5a1d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -224,7 +224,7 @@
         // audio sharing is enabled.
         if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
                 && state == BluetoothAdapter.STATE_DISCONNECTED
-                && BluetoothUtils.isAudioSharingEnabled()) {
+                && BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
             LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
             if (profileManager != null
                     && profileManager.getLeAudioBroadcastProfile() != null
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 612c193..a87b815 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -64,6 +64,8 @@
 
     public static final int META_INT_ERROR = -1;
     public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
+    public static final String DEVELOPER_OPTION_PREVIEW_KEY =
+            "bluetooth_le_audio_sharing_ui_preview_enabled";
     private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
     private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
     private static final Set<Integer> SA_PROFILES =
@@ -643,6 +645,12 @@
                 && connectedGroupIds.contains(groupId);
     }
 
+    /** Returns if the le audio sharing UI is available. */
+    public static boolean isAudioSharingUIAvailable(@Nullable Context context) {
+        return isAudioSharingEnabled() || (context != null && isAudioSharingPreviewEnabled(
+                context.getContentResolver()));
+    }
+
     /** Returns if the le audio sharing is enabled. */
     public static boolean isAudioSharingEnabled() {
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
@@ -653,7 +661,23 @@
                     && adapter.isLeAudioBroadcastAssistantSupported()
                             == BluetoothStatusCodes.FEATURE_SUPPORTED;
         } catch (IllegalStateException e) {
-            Log.d(TAG, "LE state is on, but there is no bluetooth service.", e);
+            Log.d(TAG, "Fail to check isAudioSharingEnabled, e = ", e);
+            return false;
+        }
+    }
+
+    /** Returns if the le audio sharing preview is enabled in developer option. */
+    public static boolean isAudioSharingPreviewEnabled(@Nullable ContentResolver contentResolver) {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        try {
+            return Flags.audioSharingDeveloperOption()
+                    && getAudioSharingPreviewValue(contentResolver)
+                    && adapter.isLeAudioBroadcastSourceSupported()
+                            == BluetoothStatusCodes.FEATURE_SUPPORTED
+                    && adapter.isLeAudioBroadcastAssistantSupported()
+                            == BluetoothStatusCodes.FEATURE_SUPPORTED;
+        } catch (IllegalStateException e) {
+            Log.d(TAG, "Fail to check isAudioSharingPreviewEnabled, e = ", e);
             return false;
         }
     }
@@ -996,6 +1020,17 @@
                 BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
     }
 
+    /** Get develop option value for audio sharing preview. */
+    @WorkerThread
+    private static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) {
+        if (contentResolver == null) return false;
+        return Settings.Global.getInt(
+                contentResolver,
+                DEVELOPER_OPTION_PREVIEW_KEY,
+                0 // value off
+        ) == 1;
+    }
+
     /** Get secondary {@link CachedBluetoothDevice} in broadcast. */
     @Nullable
     @WorkerThread
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 8641f70..d0827b3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1245,7 +1245,7 @@
      */
     public String getConnectionSummary(boolean shortSummary) {
         CharSequence summary = null;
-        if (BluetoothUtils.isAudioSharingEnabled()) {
+        if (BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
             if (mBluetoothManager == null) {
                 mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null);
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index b9f16ed..4b7cb36 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -383,11 +383,7 @@
                 preferredMainDevice.refresh();
                 hasChanged = true;
             }
-            if (isWorkProfile()) {
-                log("addMemberDevicesIntoMainDevice: skip sync source for work profile");
-            } else {
-                syncAudioSharingSourceIfNeeded(preferredMainDevice);
-            }
+            syncAudioSharingSourceIfNeeded(preferredMainDevice);
         }
         if (hasChanged) {
             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
@@ -402,8 +398,12 @@
     }
 
     private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) {
-        boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingEnabled();
+        boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext);
         if (isAudioSharingEnabled) {
+            if (isWorkProfile()) {
+                log("addMemberDevicesIntoMainDevice: skip sync source for work profile");
+                return;
+            }
             boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager)
                     && BluetoothUtils.hasConnectedBroadcastSource(
                     mainDevice, mBtManager);
@@ -433,6 +433,8 @@
                     }
                 }
             }
+        } else {
+            log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled");
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index edd49c5..0209eb8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.ServiceConnection
+import android.os.DeadObjectException
 import android.os.IBinder
 import android.os.IInterface
 import android.os.RemoteException
@@ -52,6 +53,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.filterIsInstance
@@ -304,6 +306,14 @@
                 service.registerDeviceSettingsListener(deviceInfo, listener)
                 awaitClose { service.unregisterDeviceSettingsListener(deviceInfo, listener) }
             }
+            .catch { e ->
+                if (e is DeadObjectException) {
+                    Log.e(TAG, "DeadObjectException happens when registering listener.", e)
+                    emit(listOf())
+                } else {
+                    throw e
+                }
+            }
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 0e060df..6d481db 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -29,11 +29,13 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -48,6 +50,7 @@
 
 import com.android.internal.R;
 import com.android.settingslib.flags.Flags;
+import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settingslib.widget.AdaptiveIcon;
 
 import com.google.common.collect.ImmutableList;
@@ -61,6 +64,8 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -69,6 +74,7 @@
 import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
 public class BluetoothUtilsTest {
 
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -88,6 +94,7 @@
     @Mock private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState;
 
     private Context mContext;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
     private static final String STRING_METADATA = "string_metadata";
     private static final String BOOL_METADATA = "true";
     private static final String INT_METADATA = "25";
@@ -109,6 +116,7 @@
 
         mContext = spy(RuntimeEnvironment.application);
         mSetFlagsRule.disableFlags(FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA);
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
         when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
@@ -1123,4 +1131,129 @@
                                 AudioDeviceInfo.TYPE_HEARING_AID,
                                 address));
     }
+
+    @Test
+    public void isAudioSharingEnabled_flagOff_returnsFalse() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        assertThat(BluetoothUtils.isAudioSharingEnabled()).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingEnabled_featureNotSupported_returnsFalse() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+
+        assertThat(BluetoothUtils.isAudioSharingEnabled()).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingEnabled_featureSupported_returnsTrue() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+
+        assertThat(BluetoothUtils.isAudioSharingEnabled()).isTrue();
+    }
+
+    @Test
+    public void isAudioSharingPreviewEnabled_flagOff_returnsFalse() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+
+        assertThat(BluetoothUtils.isAudioSharingPreviewEnabled(
+                mContext.getContentResolver())).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingPreviewEnabled_featureNotSupported_returnsFalse() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+
+        assertThat(BluetoothUtils.isAudioSharingPreviewEnabled(
+                mContext.getContentResolver())).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingPreviewEnabled_developerOptionOff_returnsFalse() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 0);
+
+        assertThat(BluetoothUtils.isAudioSharingPreviewEnabled(
+                mContext.getContentResolver())).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingPreviewEnabled_developerOptionOn_returnsTrue() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 1);
+
+        assertThat(BluetoothUtils.isAudioSharingPreviewEnabled(
+                mContext.getContentResolver())).isTrue();
+    }
+
+    @Test
+    public void isAudioSharingUIAvailable_audioSharingAndPreviewFlagOff_returnsFalse() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+
+        assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingUIAvailable_audioSharingAndPreviewDisabled_returnsFalse() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+
+        assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isFalse();
+    }
+
+    @Test
+    public void isAudioSharingUIAvailable_audioSharingEnabled_returnsTrue() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 0);
+
+        assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isTrue();
+    }
+
+    @Test
+    public void isAudioSharingUIAvailable_audioSharingPreviewEnabled_returnsTrue() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION);
+        Settings.Global.putInt(mContext.getContentResolver(),
+                BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 1);
+
+        assertThat(BluetoothUtils.isAudioSharingUIAvailable(mContext)).isTrue();
+    }
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 064198f..927a1c59 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -284,5 +284,6 @@
         Settings.Secure.MANDATORY_BIOMETRICS,
         Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
         Settings.Secure.ADVANCED_PROTECTION_MODE,
+        Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index c002a04..6d73ee2 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -332,6 +332,9 @@
         VALIDATORS.put(
                 Secure.ACCESSIBILITY_QS_TARGETS,
                 ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
+                ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ONE_HANDED_MODE_ACTIVATED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ONE_HANDED_MODE_ENABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 2034f36..fb0aaf8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1823,6 +1823,9 @@
                 Settings.Secure.ACCESSIBILITY_QS_TARGETS,
                 SecureSettingsProto.Accessibility.QS_TARGETS);
         dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
+                SecureSettingsProto.Accessibility.ACCESSIBILITY_KEY_GESTURE_TARGETS);
+        dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
                 SecureSettingsProto.Accessibility.ACCESSIBILITY_MAGNIFICATION_CAPABILITY);
         dumpSetting(s, p,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 2c8c261..7b6321d 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -263,6 +263,8 @@
     <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" />
+    <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" />
+    <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" />
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
     <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" />
     <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
@@ -957,6 +959,13 @@
     <!-- Permission required for CTS test - CtsTelephonyTestCases -->
     <uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" />
 
+    <!-- Permission required for ExecutableMethodFileOffsetsTest -->
+    <uses-permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" />
+
+    <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+    <uses-permission android:name="android.permission.READ_SYSTEM_PREFERENCES" />
+    <uses-permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index b4d81d6..7c478ac 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -353,7 +353,7 @@
     public void onDestroy() {
         mServiceHandler.getLooper().quit();
         mScreenshotHandler.getLooper().quit();
-        mBugreportSingleThreadExecutor.close();
+        mBugreportSingleThreadExecutor.shutdown();
         super.onDestroy();
     }
 
diff --git a/packages/SystemUI/README.md b/packages/SystemUI/README.md
index 2910bba..635a97e 100644
--- a/packages/SystemUI/README.md
+++ b/packages/SystemUI/README.md
@@ -87,7 +87,7 @@
 
 There are a few places where CommandQueue is used as a bus to communicate
 across sysui. Such as when StatusBar calls CommandQueue#recomputeDisableFlags.
-This is generally used a shortcut to directly trigger CommandQueue rather than
+This is generally used as a shortcut to directly trigger CommandQueue rather than
 calling StatusManager and waiting for the call to come back to IStatusBar.
 
 ### [com.android.systemui.util.NotificationChannels](/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java)
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1c846a6..3df9603 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -16,6 +16,16 @@
 }
 
 flag {
+   name: "multiuser_wifi_picker_tracker_support"
+   namespace: "systemui"
+   description: "Adds WifiPickerTracker support for multiple users to support when HSUM is enabled."
+   bug: "371586248"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "udfps_view_performance"
    namespace: "systemui"
    description: "Decrease screen off blocking calls by waiting until the device is finished going to sleep before adding the udfps view."
@@ -257,7 +267,7 @@
 flag {
     name: "dual_shade"
     namespace: "systemui"
-    description: "Enables the BC25 Dual Shade (go/bc25-dual-shade-design)."
+    description: "Enables Dual Shade (go/dual-shade-design-doc)."
     bug: "337259436"
 }
 
@@ -414,6 +424,17 @@
 }
 
 flag {
+    name: "status_bar_auto_start_screen_record_chip"
+    namespace: "systemui"
+    description: "When screen recording, use the specified start time to update the screen record "
+        "chip state instead of waiting for an official 'recording started' signal"
+    bug: "366448907"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "status_bar_use_repos_for_call_chip"
     namespace: "systemui"
     description: "Use repositories as the source of truth for call notifications shown as a chip in"
@@ -620,9 +641,9 @@
 }
 
 flag {
-    name: "status_bar_simple_fragment"
+    name: "status_bar_root_modernization"
     namespace: "systemui"
-    description: "Feature flag for refactoring the collapsed status bar fragment"
+    description: "Feature flag for replacing the status bar fragment with a compose root"
     bug: "364360986"
 }
 
@@ -1339,16 +1360,6 @@
 }
 
 flag {
-  name: "notification_pulsing_fix"
-  namespace: "systemui"
-  description: "Allow showing new pulsing notifications when the device is already pulsing."
-  bug: "335560575"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
     name: "media_lockscreen_launch_animation"
     namespace : "systemui"
     description : "Enable the origin launch animation for UMO when opening on top of lockscreen."
@@ -1763,3 +1774,13 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "keyguard_transition_force_finish_on_screen_off"
+    namespace: "systemui"
+    description: "Forces KTF transitions to finish if the screen turns all the way off."
+    bug: "331636736"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 00d9056..300bdf2 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -48,7 +48,7 @@
                 Bound.LEFT to createViewProperty(Bound.LEFT),
                 Bound.TOP to createViewProperty(Bound.TOP),
                 Bound.RIGHT to createViewProperty(Bound.RIGHT),
-                Bound.BOTTOM to createViewProperty(Bound.BOTTOM)
+                Bound.BOTTOM to createViewProperty(Bound.BOTTOM),
             )
 
         private fun createViewProperty(bound: Bound): IntProperty<View> {
@@ -89,7 +89,7 @@
             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
             duration: Long = DEFAULT_DURATION,
             animateChildren: Boolean = true,
-            excludedViews: Set<View> = emptySet()
+            excludedViews: Set<View> = emptySet(),
         ): Boolean {
             return animate(
                 rootView,
@@ -97,7 +97,7 @@
                 duration,
                 ephemeral = false,
                 animateChildren = animateChildren,
-                excludedViews = excludedViews
+                excludedViews = excludedViews,
             )
         }
 
@@ -111,7 +111,7 @@
             interpolator: Interpolator = DEFAULT_INTERPOLATOR,
             duration: Long = DEFAULT_DURATION,
             animateChildren: Boolean = true,
-            excludedViews: Set<View> = emptySet()
+            excludedViews: Set<View> = emptySet(),
         ): Boolean {
             return animate(
                 rootView,
@@ -119,7 +119,7 @@
                 duration,
                 ephemeral = true,
                 animateChildren = animateChildren,
-                excludedViews = excludedViews
+                excludedViews = excludedViews,
             )
         }
 
@@ -129,7 +129,7 @@
             duration: Long,
             ephemeral: Boolean,
             animateChildren: Boolean,
-            excludedViews: Set<View> = emptySet()
+            excludedViews: Set<View> = emptySet(),
         ): Boolean {
             if (
                 !occupiesSpace(
@@ -137,7 +137,7 @@
                     rootView.left,
                     rootView.top,
                     rootView.right,
-                    rootView.bottom
+                    rootView.bottom,
                 )
             ) {
                 return false
@@ -149,7 +149,7 @@
                 listener,
                 recursive = true,
                 animateChildren = animateChildren,
-                excludedViews = excludedViews
+                excludedViews = excludedViews,
             )
             return true
         }
@@ -164,7 +164,7 @@
         private fun createUpdateListener(
             interpolator: Interpolator,
             duration: Long,
-            ephemeral: Boolean
+            ephemeral: Boolean,
         ): View.OnLayoutChangeListener {
             return createListener(interpolator, duration, ephemeral)
         }
@@ -196,9 +196,9 @@
          *
          * @param includeFadeIn true if the animator should also fade in the view and child views.
          * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if
-         *     [includeFadeIn] is false.
-         * @param onAnimationEnd an optional runnable that will be run once the animation
-         *    finishes successfully. Will not be run if the animation is cancelled.
+         *   [includeFadeIn] is false.
+         * @param onAnimationEnd an optional runnable that will be run once the animation finishes,
+         *   regardless of whether the animation is cancelled or finishes successfully.
          */
         @JvmOverloads
         fun animateAddition(
@@ -217,7 +217,7 @@
                     rootView.left,
                     rootView.top,
                     rootView.right,
-                    rootView.bottom
+                    rootView.bottom,
                 )
             ) {
                 return false
@@ -241,7 +241,10 @@
                 // First, fade in the container view
                 val containerDuration = duration / 6
                 createAndStartFadeInAnimator(
-                    rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator
+                    rootView,
+                    containerDuration,
+                    startDelay = 0,
+                    interpolator = fadeInInterpolator,
                 )
 
                 // Then, fade in the child views
@@ -253,7 +256,7 @@
                         childDuration,
                         // Wait until the container fades in before fading in the children
                         startDelay = containerDuration,
-                        interpolator = fadeInInterpolator
+                        interpolator = fadeInInterpolator,
                     )
                 }
                 // For now, we don't recursively fade in additional sub views (e.g. grandchild
@@ -264,7 +267,7 @@
                     rootView,
                     duration / 2,
                     startDelay = 0,
-                    interpolator = fadeInInterpolator
+                    interpolator = fadeInInterpolator,
                 )
             }
 
@@ -323,7 +326,7 @@
                     previousLeft: Int,
                     previousTop: Int,
                     previousRight: Int,
-                    previousBottom: Int
+                    previousBottom: Int,
                 ) {
                     if (view == null) return
 
@@ -353,14 +356,14 @@
                             startTop,
                             startRight,
                             startBottom,
-                            ignorePreviousValues
+                            ignorePreviousValues,
                         )
                     val endValues =
                         mapOf(
                             Bound.LEFT to left,
                             Bound.TOP to top,
                             Bound.RIGHT to right,
-                            Bound.BOTTOM to bottom
+                            Bound.BOTTOM to bottom,
                         )
 
                     val boundsToAnimate = mutableSetOf<Bound>()
@@ -396,8 +399,8 @@
          * added on the side(s) of the [destination], the translation of those margins can be
          * included by specifying [includeMargins].
          *
-         * @param onAnimationEnd an optional runnable that will be run once the animation finishes
-         *    successfully. Will not be run if the animation is cancelled.
+         * @param onAnimationEnd an optional runnable that will be run once the animation finishes,
+         *   regardless of whether the animation is cancelled or finishes successfully.
          */
         @JvmOverloads
         fun animateRemoval(
@@ -414,7 +417,7 @@
                     rootView.left,
                     rootView.top,
                     rootView.right,
-                    rootView.bottom
+                    rootView.bottom,
                 )
             ) {
                 return false
@@ -458,7 +461,7 @@
                     Bound.LEFT to rootView.left,
                     Bound.TOP to rootView.top,
                     Bound.RIGHT to rootView.right,
-                    Bound.BOTTOM to rootView.bottom
+                    Bound.BOTTOM to rootView.bottom,
                 )
             val endValues =
                 processEndValuesForRemoval(
@@ -550,7 +553,7 @@
             destination: Hotspot,
             endValues: Map<Bound, Int>,
             interpolator: Interpolator,
-            duration: Long
+            duration: Long,
         ) {
             for (i in 0 until rootView.childCount) {
                 val child = rootView.getChildAt(i)
@@ -559,7 +562,7 @@
                         Bound.LEFT to child.left,
                         Bound.TOP to child.top,
                         Bound.RIGHT to child.right,
-                        Bound.BOTTOM to child.bottom
+                        Bound.BOTTOM to child.bottom,
                     )
                 val childEndValues =
                     processChildEndValuesForRemoval(
@@ -569,7 +572,7 @@
                         child.right,
                         child.bottom,
                         endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT),
-                        endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP)
+                        endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP),
                     )
 
                 val boundsToAnimate = mutableSetOf<Bound>()
@@ -587,7 +590,7 @@
                     childEndValues,
                     interpolator,
                     duration,
-                    ephemeral = true
+                    ephemeral = true,
                 )
             }
         }
@@ -601,7 +604,7 @@
             left: Int,
             top: Int,
             right: Int,
-            bottom: Int
+            bottom: Int,
         ): Boolean {
             return visibility != View.GONE && left != right && top != bottom
         }
@@ -616,6 +619,7 @@
          * not newly introduced margins are included.
          *
          * Base case
+         *
          * ```
          *     1) origin=TOP
          *         x---------x    x---------x    x---------x    x---------x    x---------x
@@ -636,9 +640,11 @@
          *                                         x-----x       x-------x     |         |
          *                                                                     x---------x
          * ```
+         *
          * In case the start and end values differ in the direction of the origin, and
          * [ignorePreviousValues] is false, the previous values are used and a translation is
          * included in addition to the view expansion.
+         *
          * ```
          *     origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70)
          *         x
@@ -660,7 +666,7 @@
             previousTop: Int,
             previousRight: Int,
             previousBottom: Int,
-            ignorePreviousValues: Boolean
+            ignorePreviousValues: Boolean,
         ): Map<Bound, Int> {
             val startLeft = if (ignorePreviousValues) newLeft else previousLeft
             val startTop = if (ignorePreviousValues) newTop else previousTop
@@ -727,7 +733,7 @@
                 Bound.LEFT to left,
                 Bound.TOP to top,
                 Bound.RIGHT to right,
-                Bound.BOTTOM to bottom
+                Bound.BOTTOM to bottom,
             )
         }
 
@@ -777,18 +783,17 @@
             includeMargins: Boolean = false,
         ): Map<Bound, Int> {
             val marginAdjustment =
-                if (includeMargins &&
-                    (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
+                if (includeMargins && (rootView.layoutParams is ViewGroup.MarginLayoutParams)) {
                     val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams
                     DimenHolder(
                         left = marginLp.leftMargin,
                         top = marginLp.topMargin,
                         right = marginLp.rightMargin,
-                        bottom = marginLp.bottomMargin
+                        bottom = marginLp.bottomMargin,
                     )
-            } else {
-                DimenHolder(0, 0, 0, 0)
-            }
+                } else {
+                    DimenHolder(0, 0, 0, 0)
+                }
 
             // These are the end values to use *if* this bound is part of the destination.
             val endLeft = left - marginAdjustment.left
@@ -805,60 +810,69 @@
             //  - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight.
 
             return when (destination) {
-                Hotspot.TOP -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.LEFT to left,
-                    Bound.RIGHT to right,
-                )
-                Hotspot.TOP_RIGHT -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                )
-                Hotspot.RIGHT -> mapOf(
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                    Bound.TOP to top,
-                    Bound.BOTTOM to bottom,
-                )
-                Hotspot.BOTTOM_RIGHT -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.RIGHT to endRight,
-                    Bound.LEFT to endRight,
-                )
-                Hotspot.BOTTOM -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.LEFT to left,
-                    Bound.RIGHT to right,
-                )
-                Hotspot.BOTTOM_LEFT -> mapOf(
-                    Bound.BOTTOM to endBottom,
-                    Bound.TOP to endBottom,
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                )
-                Hotspot.LEFT -> mapOf(
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                    Bound.TOP to top,
-                    Bound.BOTTOM to bottom,
-                )
-                Hotspot.TOP_LEFT -> mapOf(
-                    Bound.TOP to endTop,
-                    Bound.BOTTOM to endTop,
-                    Bound.LEFT to endLeft,
-                    Bound.RIGHT to endLeft,
-                )
-                Hotspot.CENTER -> mapOf(
-                    Bound.LEFT to (endLeft + endRight) / 2,
-                    Bound.RIGHT to (endLeft + endRight) / 2,
-                    Bound.TOP to (endTop + endBottom) / 2,
-                    Bound.BOTTOM to (endTop + endBottom) / 2,
-                )
+                Hotspot.TOP ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.LEFT to left,
+                        Bound.RIGHT to right,
+                    )
+                Hotspot.TOP_RIGHT ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                    )
+                Hotspot.RIGHT ->
+                    mapOf(
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                        Bound.TOP to top,
+                        Bound.BOTTOM to bottom,
+                    )
+                Hotspot.BOTTOM_RIGHT ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.RIGHT to endRight,
+                        Bound.LEFT to endRight,
+                    )
+                Hotspot.BOTTOM ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.LEFT to left,
+                        Bound.RIGHT to right,
+                    )
+                Hotspot.BOTTOM_LEFT ->
+                    mapOf(
+                        Bound.BOTTOM to endBottom,
+                        Bound.TOP to endBottom,
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                    )
+                Hotspot.LEFT ->
+                    mapOf(
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                        Bound.TOP to top,
+                        Bound.BOTTOM to bottom,
+                    )
+                Hotspot.TOP_LEFT ->
+                    mapOf(
+                        Bound.TOP to endTop,
+                        Bound.BOTTOM to endTop,
+                        Bound.LEFT to endLeft,
+                        Bound.RIGHT to endLeft,
+                    )
+                Hotspot.CENTER ->
+                    mapOf(
+                        Bound.LEFT to (endLeft + endRight) / 2,
+                        Bound.RIGHT to (endLeft + endRight) / 2,
+                        Bound.TOP to (endTop + endBottom) / 2,
+                        Bound.BOTTOM to (endTop + endBottom) / 2,
+                    )
             }
         }
 
@@ -887,7 +901,7 @@
             right: Int,
             bottom: Int,
             parentWidth: Int,
-            parentHeight: Int
+            parentHeight: Int,
         ): Map<Bound, Int> {
             val halfWidth = (right - left) / 2
             val halfHeight = (bottom - top) / 2
@@ -945,7 +959,7 @@
                 Bound.LEFT to endLeft,
                 Bound.TOP to endTop,
                 Bound.RIGHT to endRight,
-                Bound.BOTTOM to endBottom
+                Bound.BOTTOM to endBottom,
             )
         }
 
@@ -954,7 +968,7 @@
             listener: View.OnLayoutChangeListener,
             recursive: Boolean = false,
             animateChildren: Boolean = true,
-            excludedViews: Set<View> = emptySet()
+            excludedViews: Set<View> = emptySet(),
         ) {
             if (excludedViews.contains(view)) return
 
@@ -973,7 +987,7 @@
                         listener,
                         recursive = true,
                         animateChildren = animateChildren,
-                        excludedViews = excludedViews
+                        excludedViews = excludedViews,
                     )
                 }
             }
@@ -1027,7 +1041,7 @@
                                 PropertyValuesHolder.ofInt(
                                     PROPERTIES[bound],
                                     startValues.getValue(bound),
-                                    endValues.getValue(bound)
+                                    endValues.getValue(bound),
                                 )
                             )
                         }
@@ -1056,9 +1070,10 @@
                             // listener.
                             recursivelyRemoveListener(view)
                         }
-                        if (!cancelled) {
-                            onAnimationEnd?.run()
-                        }
+                        // Run the end runnable regardless of whether the animation was cancelled or
+                        // not - this ensures critical actions (like removing a window) always occur
+                        // (see b/344049884).
+                        onAnimationEnd?.run()
                     }
 
                     override fun onAnimationCancel(animation: Animator) {
@@ -1077,17 +1092,19 @@
             view: View,
             duration: Long,
             startDelay: Long,
-            interpolator: Interpolator
+            interpolator: Interpolator,
         ) {
             val animator = ObjectAnimator.ofFloat(view, "alpha", 1f)
             animator.startDelay = startDelay
             animator.duration = duration
             animator.interpolator = interpolator
-            animator.addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    view.setTag(R.id.tag_alpha_animator, null /* tag */)
+            animator.addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        view.setTag(R.id.tag_alpha_animator, null /* tag */)
+                    }
                 }
-            })
+            )
 
             (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel()
             view.setTag(R.id.tag_alpha_animator, animator)
@@ -1105,7 +1122,7 @@
         RIGHT,
         BOTTOM_RIGHT,
         BOTTOM,
-        BOTTOM_LEFT
+        BOTTOM_LEFT,
     }
 
     private enum class Bound(val label: String, val overrideTag: Int) {
@@ -1147,14 +1164,10 @@
         };
 
         abstract fun setValue(view: View, value: Int)
+
         abstract fun getValue(view: View): Int
     }
 
     /** Simple data class to hold a set of dimens for left, top, right, bottom. */
-    private data class DimenHolder(
-        val left: Int,
-        val top: Int,
-        val right: Int,
-        val bottom: Int,
-    )
+    private data class DimenHolder(val left: Int, val top: Int, val right: Int, val bottom: Int)
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
index d58e1bf..163f4b3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
@@ -59,6 +59,7 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.hideFromAccessibility
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
@@ -286,9 +287,12 @@
     Surface(
         modifier =
             Modifier.padding(top = 16.dp, bottom = 6.dp)
-                .semantics { contentDescription = dragHandleContentDescription }
+                .semantics {
+                    contentDescription = dragHandleContentDescription
+                    hideFromAccessibility()
+                }
                 .clickable { dialog.dismiss() },
-        color = MaterialTheme.colorScheme.outlineVariant,
+        color = MaterialTheme.colorScheme.onSurfaceVariant,
         shape = MaterialTheme.shapes.extraLarge,
     ) {
         Box(Modifier.size(width = 32.dp, height = 4.dp))
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index e7b66c5..d976e8e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -693,8 +693,8 @@
     val fromState = updateStateInContent(transition.fromContent)
     val toState = updateStateInContent(transition.toContent)
 
-    reconcileStates(element, previousTransition)
-    reconcileStates(element, transition)
+    val previousUniqueState = reconcileStates(element, previousTransition, previousState = null)
+    reconcileStates(element, transition, previousState = previousUniqueState)
 
     // Remove the interruption values to all contents but the content(s) where the element will be
     // placed, to make sure that interruption deltas are computed only right after this interruption
@@ -721,12 +721,32 @@
 /**
  * Reconcile the state of [element] in the formContent and toContent of [transition] so that the
  * values before interruption have their expected values, taking shared transitions into account.
+ *
+ * @return the unique state this element had during [transition], `null` if it had multiple
+ *   different states (i.e. the shared animation was disabled).
  */
-private fun reconcileStates(element: Element, transition: TransitionState.Transition) {
-    val fromContentState = element.stateByContent[transition.fromContent] ?: return
-    val toContentState = element.stateByContent[transition.toContent] ?: return
+private fun reconcileStates(
+    element: Element,
+    transition: TransitionState.Transition,
+    previousState: Element.State?,
+): Element.State? {
+    fun reconcileWithPreviousState(state: Element.State) {
+        if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) {
+            state.updateValuesBeforeInterruption(previousState)
+        }
+    }
+
+    val fromContentState = element.stateByContent[transition.fromContent]
+    val toContentState = element.stateByContent[transition.toContent]
+
+    if (fromContentState == null || toContentState == null) {
+        return (fromContentState ?: toContentState)
+            ?.also { reconcileWithPreviousState(it) }
+            ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified }
+    }
+
     if (!isSharedElementEnabled(element.key, transition)) {
-        return
+        return null
     }
 
     if (
@@ -735,13 +755,19 @@
     ) {
         // Element is shared and placed in fromContent only.
         toContentState.updateValuesBeforeInterruption(fromContentState)
-    } else if (
+        return fromContentState
+    }
+
+    if (
         toContentState.offsetBeforeInterruption != Offset.Unspecified &&
             fromContentState.offsetBeforeInterruption == Offset.Unspecified
     ) {
         // Element is shared and placed in toContent only.
         fromContentState.updateValuesBeforeInterruption(toContentState)
+        return toContentState
     }
+
+    return null
 }
 
 private fun Element.State.selfUpdateValuesBeforeInterruption() {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 4a90515..a301856 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2638,4 +2638,58 @@
             assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0)
         }
     }
+
+    @Test
+    fun interruption_considerPreviousUniqueState() {
+        @Composable
+        fun SceneScope.Foo(modifier: Modifier = Modifier) {
+            Box(modifier.element(TestElements.Foo).size(50.dp))
+        }
+
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+        val scope =
+            rule.setContentAndCreateMainScope {
+                SceneTransitionLayout(state) {
+                    scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
+                    scene(SceneB) { Box(Modifier.fillMaxSize()) }
+                    scene(SceneC) {
+                        Box(Modifier.fillMaxSize()) { Foo(Modifier.offset(x = 100.dp, y = 100.dp)) }
+                    }
+                }
+            }
+
+        // During A => B, Foo disappears and stays in its original position.
+        scope.launch { state.startTransition(transition(SceneA, SceneB)) }
+        rule
+            .onNode(isElement(TestElements.Foo))
+            .assertSizeIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        // Interrupt A => B by B => C.
+        var interruptionProgress by mutableFloatStateOf(1f)
+        scope.launch {
+            state.startTransition(
+                transition(SceneB, SceneC, interruptionProgress = { interruptionProgress })
+            )
+        }
+
+        // During B => C, Foo appears again. It is still at (0, 0) when the interruption progress is
+        // 100%, and converges to its position (100, 100) in C.
+        rule
+            .onNode(isElement(TestElements.Foo))
+            .assertSizeIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        interruptionProgress = 0.5f
+        rule
+            .onNode(isElement(TestElements.Foo))
+            .assertSizeIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+
+        interruptionProgress = 0f
+        rule
+            .onNode(isElement(TestElements.Foo))
+            .assertSizeIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(100.dp, 100.dp)
+    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
index ae18aac..052e60e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt
@@ -25,6 +25,9 @@
     /** Returns a [Flow] tracking the value of a setting as an [Int]. */
     fun intSetting(name: String, defaultValue: Int = 0): Flow<Int>
 
+    /** Returns a [Flow] tracking the value of a setting as a [Boolean]. */
+    fun boolSetting(name: String, defaultValue: Boolean = false): Flow<Boolean>
+
     /** Updates the value of the setting with the given name. */
     suspend fun setInt(name: String, value: Int)
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt
index 8b9fcb4..9f37959 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt
@@ -63,6 +63,10 @@
             .flowOn(backgroundDispatcher)
     }
 
+    override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> {
+        return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 }
+    }
+
     override suspend fun setInt(name: String, value: Int) {
         withContext(backgroundDispatcher) { Settings.Secure.putInt(contentResolver, name, value) }
     }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
index 8cda9b3..b5f991c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt
@@ -25,6 +25,9 @@
     /** Returns a [Flow] tracking the value of a setting as an [Int]. */
     fun intSetting(name: String, defaultValue: Int = 0): Flow<Int>
 
+    /** Returns a [Flow] tracking the value of a setting as a [Boolean]. */
+    fun boolSetting(name: String, defaultValue: Boolean = false): Flow<Boolean>
+
     /** Updates the value of the setting with the given name. */
     suspend fun setInt(name: String, value: Int)
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt
index b039a32..8485d4d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt
@@ -63,6 +63,10 @@
             .flowOn(backgroundDispatcher)
     }
 
+    override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> {
+        return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 }
+    }
+
     override suspend fun setInt(name: String, value: Int) {
         withContext(backgroundDispatcher) { Settings.System.putInt(contentResolver, name, value) }
     }
diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt
index 37b9792..21d8d46 100644
--- a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt
+++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt
@@ -28,6 +28,10 @@
         return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() }
     }
 
+    override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> {
+        return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 }
+    }
+
     override suspend fun setInt(name: String, value: Int) {
         settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() }
     }
diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
index 7da2b40..f6c053f 100644
--- a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
+++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt
@@ -28,6 +28,10 @@
         return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() }
     }
 
+    override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> {
+        return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 }
+    }
+
     override suspend fun setInt(name: String, value: Int) {
         settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index a8c3af9..8db82d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -24,8 +24,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
-class
-ViewHierarchyAnimatorTest : SysuiTestCase() {
+class ViewHierarchyAnimatorTest : SysuiTestCase() {
     companion object {
         private const val TEST_DURATION = 1000L
         private val TEST_INTERPOLATOR = Interpolators.LINEAR
@@ -49,9 +48,12 @@
         rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
 
         // animate()
-        var success = ViewHierarchyAnimator.animate(
-            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
-        )
+        var success =
+            ViewHierarchyAnimator.animate(
+                rootView,
+                interpolator = TEST_INTERPOLATOR,
+                duration = TEST_DURATION,
+            )
         rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -64,9 +66,12 @@
         ViewHierarchyAnimator.stopAnimating(rootView)
 
         // animateNextUpdate()
-        success = ViewHierarchyAnimator.animateNextUpdate(
-            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
-        )
+        success =
+            ViewHierarchyAnimator.animateNextUpdate(
+                rootView,
+                interpolator = TEST_INTERPOLATOR,
+                duration = TEST_DURATION,
+            )
         rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
 
         assertTrue(success)
@@ -79,9 +84,12 @@
 
         // animateAddition()
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                interpolator = TEST_INTERPOLATOR,
+                duration = TEST_DURATION,
+            )
         rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
 
         assertTrue(success)
@@ -93,9 +101,12 @@
         // animateRemoval()
         setUpRootWithChildren()
         val child = rootView.getChildAt(0)
-        success = ViewHierarchyAnimator.animateRemoval(
-            child, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                child,
+                interpolator = TEST_INTERPOLATOR,
+                duration = TEST_DURATION,
+            )
 
         assertTrue(success)
         assertNotNull(child.getTag(R.id.tag_animator))
@@ -185,7 +196,7 @@
         // Change all bounds.
         rootView.measure(
             View.MeasureSpec.makeMeasureSpec(190, View.MeasureSpec.EXACTLY),
-            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
         )
         rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
 
@@ -211,14 +222,12 @@
     fun animatesRootAndChildren_withExcludedViews() {
         setUpRootWithChildren()
 
-        val success = ViewHierarchyAnimator.animate(
-            rootView,
-            excludedViews = setOf(rootView.getChildAt(0))
-        )
+        val success =
+            ViewHierarchyAnimator.animate(rootView, excludedViews = setOf(rootView.getChildAt(0)))
         // Change all bounds.
         rootView.measure(
-                View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
         )
         rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
 
@@ -245,14 +254,11 @@
     fun animatesRootOnly() {
         setUpRootWithChildren()
 
-        val success = ViewHierarchyAnimator.animate(
-                rootView,
-                animateChildren = false
-        )
+        val success = ViewHierarchyAnimator.animate(rootView, animateChildren = false)
         // Change all bounds.
         rootView.measure(
-                View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
         )
         rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
 
@@ -351,10 +357,11 @@
     fun animatesAppearingViewsRespectingOrigin() {
         // CENTER.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        var success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.CENTER
-        )
+        var success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.CENTER,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -364,10 +371,11 @@
 
         // LEFT.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.LEFT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -377,10 +385,11 @@
 
         // TOP_LEFT.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -390,10 +399,11 @@
 
         // TOP.
         rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.TOP
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -403,10 +413,11 @@
 
         // TOP_RIGHT.
         rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -416,10 +427,11 @@
 
         // RIGHT.
         rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.RIGHT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -429,10 +441,11 @@
 
         // BOTTOM_RIGHT.
         rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -442,10 +455,11 @@
 
         // BOTTOM.
         rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -455,10 +469,11 @@
 
         // BOTTOM_LEFT.
         rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -471,11 +486,12 @@
     fun animatesAppearingViewsRespectingMargins() {
         // CENTER.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        var success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.CENTER,
-            includeMargins = true
-        )
+        var success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.CENTER,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -485,10 +501,12 @@
 
         // LEFT.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView, origin = ViewHierarchyAnimator.Hotspot.LEFT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.LEFT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -498,11 +516,12 @@
 
         // TOP_LEFT.
         rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -512,10 +531,12 @@
 
         // TOP.
         rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView, origin = ViewHierarchyAnimator.Hotspot.TOP,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -525,11 +546,12 @@
 
         // TOP_RIGHT.
         rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -539,11 +561,12 @@
 
         // RIGHT.
         rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.RIGHT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.RIGHT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -553,11 +576,12 @@
 
         // BOTTOM_RIGHT.
         rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -567,11 +591,12 @@
 
         // BOTTOM.
         rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -581,11 +606,12 @@
 
         // BOTTOM_LEFT.
         rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */)
-        success = ViewHierarchyAnimator.animateAddition(
-            rootView,
-            origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
-            includeMargins = true
-        )
+        success =
+            ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+                includeMargins = true,
+            )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         assertTrue(success)
@@ -626,7 +652,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = true,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -641,7 +667,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = true,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -663,7 +689,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = true,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -680,7 +706,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = true,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -692,7 +718,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = true,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
 
         // THEN the alpha remains at its current value (it doesn't get reset to 0)
@@ -721,7 +747,7 @@
         ViewHierarchyAnimator.animateAddition(
             rootView,
             includeFadeIn = false,
-            fadeInInterpolator = Interpolators.LINEAR
+            fadeInInterpolator = Interpolators.LINEAR,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -738,10 +764,10 @@
         val onAnimationEndRunnable = { runnableRun = true }
 
         ViewHierarchyAnimator.animateAddition(
-                rootView,
-                origin = ViewHierarchyAnimator.Hotspot.CENTER,
-                includeMargins = true,
-                onAnimationEnd = onAnimationEndRunnable
+            rootView,
+            origin = ViewHierarchyAnimator.Hotspot.CENTER,
+            includeMargins = true,
+            onAnimationEnd = onAnimationEndRunnable,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -751,7 +777,7 @@
     }
 
     @Test
-    fun animateAddition_runnableDoesNotRunWhenAnimationCancelled() {
+    fun animateAddition_runnableRunsWhenAnimationCancelled() {
         var runnableRun = false
         val onAnimationEndRunnable = { runnableRun = true }
 
@@ -759,13 +785,13 @@
             rootView,
             origin = ViewHierarchyAnimator.Hotspot.CENTER,
             includeMargins = true,
-            onAnimationEnd = onAnimationEndRunnable
+            onAnimationEnd = onAnimationEndRunnable,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
         cancelAnimation(rootView)
 
-        assertEquals(false, runnableRun)
+        assertEquals(true, runnableRun)
     }
 
     @Test
@@ -777,7 +803,7 @@
             rootView,
             origin = ViewHierarchyAnimator.Hotspot.CENTER,
             includeMargins = true,
-            onAnimationEnd = onAnimationEndRunnable
+            onAnimationEnd = onAnimationEndRunnable,
         )
         rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
 
@@ -791,11 +817,12 @@
         setUpRootWithChildren()
 
         val child = rootView.getChildAt(0)
-        val success = ViewHierarchyAnimator.animateRemoval(
-            child,
-            destination = ViewHierarchyAnimator.Hotspot.LEFT,
-            interpolator = Interpolators.LINEAR
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                child,
+                destination = ViewHierarchyAnimator.Hotspot.LEFT,
+                interpolator = Interpolators.LINEAR,
+            )
 
         assertTrue(success)
         assertNotNull(child.getTag(R.id.tag_animator))
@@ -820,11 +847,12 @@
         rootView.addView(onlyChild)
         forceLayout()
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            onlyChild,
-            destination = ViewHierarchyAnimator.Hotspot.LEFT,
-            interpolator = Interpolators.LINEAR
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                onlyChild,
+                destination = ViewHierarchyAnimator.Hotspot.LEFT,
+                interpolator = Interpolators.LINEAR,
+            )
 
         assertTrue(success)
         assertNotNull(onlyChild.getTag(R.id.tag_animator))
@@ -845,9 +873,11 @@
         setUpRootWithChildren()
         var removedChild = rootView.getChildAt(0)
         var remainingChild = rootView.getChildAt(1)
-        var success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.CENTER
-        )
+        var success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.CENTER,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -863,9 +893,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.LEFT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -881,9 +913,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -899,9 +933,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -917,9 +953,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -935,9 +973,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.RIGHT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -953,9 +993,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -971,9 +1013,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -989,9 +1033,11 @@
         setUpRootWithChildren()
         removedChild = rootView.getChildAt(0)
         remainingChild = rootView.getChildAt(1)
-        success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT
-        )
+        success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+            )
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -1014,11 +1060,12 @@
         val originalRight = removedChild.right
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.CENTER,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.CENTER,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1027,13 +1074,7 @@
         val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2
         val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2
 
-        checkBounds(
-            removedChild,
-            l = expectedX,
-            t = expectedY,
-            r = expectedX,
-            b = expectedY
-        )
+        checkBounds(removedChild, l = expectedX, t = expectedY, r = expectedX, b = expectedY)
     }
 
     @Test
@@ -1044,11 +1085,12 @@
         val originalTop = removedChild.top
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.LEFT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.LEFT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1059,7 +1101,7 @@
             l = originalLeft - M_LEFT,
             t = originalTop,
             r = originalLeft - M_LEFT,
-            b = originalBottom
+            b = originalBottom,
         )
     }
 
@@ -1070,11 +1112,12 @@
         val originalLeft = removedChild.left
         val originalTop = removedChild.top
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1085,7 +1128,7 @@
             l = originalLeft - M_LEFT,
             t = originalTop - M_TOP,
             r = originalLeft - M_LEFT,
-            b = originalTop - M_TOP
+            b = originalTop - M_TOP,
         )
     }
 
@@ -1097,11 +1140,12 @@
         val originalTop = removedChild.top
         val originalRight = removedChild.right
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.TOP,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1112,7 +1156,7 @@
             l = originalLeft,
             t = originalTop - M_TOP,
             r = originalRight,
-            b = originalTop - M_TOP
+            b = originalTop - M_TOP,
         )
     }
 
@@ -1123,11 +1167,12 @@
         val originalTop = removedChild.top
         val originalRight = removedChild.right
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1138,7 +1183,7 @@
             l = originalRight + M_RIGHT,
             t = originalTop - M_TOP,
             r = originalRight + M_RIGHT,
-            b = originalTop - M_TOP
+            b = originalTop - M_TOP,
         )
     }
 
@@ -1150,11 +1195,12 @@
         val originalRight = removedChild.right
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.RIGHT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.RIGHT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1165,7 +1211,7 @@
             l = originalRight + M_RIGHT,
             t = originalTop,
             r = originalRight + M_RIGHT,
-            b = originalBottom
+            b = originalBottom,
         )
     }
 
@@ -1176,11 +1222,12 @@
         val originalRight = removedChild.right
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1191,7 +1238,7 @@
             l = originalRight + M_RIGHT,
             t = originalBottom + M_BOTTOM,
             r = originalRight + M_RIGHT,
-            b = originalBottom + M_BOTTOM
+            b = originalBottom + M_BOTTOM,
         )
     }
 
@@ -1203,11 +1250,12 @@
         val originalRight = removedChild.right
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1218,7 +1266,7 @@
             l = originalLeft,
             t = originalBottom + M_BOTTOM,
             r = originalRight,
-            b = originalBottom + M_BOTTOM
+            b = originalBottom + M_BOTTOM,
         )
     }
 
@@ -1229,11 +1277,12 @@
         val originalLeft = removedChild.left
         val originalBottom = removedChild.bottom
 
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild,
-            destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
-            includeMargins = true,
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(
+                removedChild,
+                destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT,
+                includeMargins = true,
+            )
         forceLayout()
 
         assertTrue(success)
@@ -1244,9 +1293,10 @@
             l = originalLeft - M_LEFT,
             t = originalBottom + M_BOTTOM,
             r = originalLeft - M_LEFT,
-            b = originalBottom + M_BOTTOM
+            b = originalBottom + M_BOTTOM,
         )
     }
+
     /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */
 
     @Test
@@ -1256,9 +1306,8 @@
         val child = rootView.getChildAt(0) as ViewGroup
         val firstGrandChild = child.getChildAt(0)
         val secondGrandChild = child.getChildAt(1)
-        val success = ViewHierarchyAnimator.animateRemoval(
-            child, interpolator = Interpolators.LINEAR
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(child, interpolator = Interpolators.LINEAR)
 
         assertTrue(success)
         assertNotNull(child.getTag(R.id.tag_animator))
@@ -1288,9 +1337,8 @@
 
         val removedChild = rootView.getChildAt(0)
         val remainingChild = rootView.getChildAt(1)
-        val success = ViewHierarchyAnimator.animateRemoval(
-            removedChild, interpolator = Interpolators.LINEAR
-        )
+        val success =
+            ViewHierarchyAnimator.animateRemoval(removedChild, interpolator = Interpolators.LINEAR)
         // Ensure that the layout happens before the checks.
         forceLayout()
 
@@ -1315,17 +1363,14 @@
         forceLayout()
         val removedView = rootView.getChildAt(0)
 
-        ViewHierarchyAnimator.animateRemoval(
-            removedView,
-            onAnimationEnd = onAnimationEndRunnable
-        )
+        ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable)
         endAnimation(removedView)
 
         assertEquals(true, runnableRun)
     }
 
     @Test
-    fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() {
+    fun animateRemoval_runnableRunsWhenAnimationCancelled() {
         var runnableRun = false
         val onAnimationEndRunnable = { runnableRun = true }
 
@@ -1333,13 +1378,10 @@
         forceLayout()
         val removedView = rootView.getChildAt(0)
 
-        ViewHierarchyAnimator.animateRemoval(
-            removedView,
-            onAnimationEnd = onAnimationEndRunnable
-        )
+        ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable)
         cancelAnimation(removedView)
 
-        assertEquals(false, runnableRun)
+        assertEquals(true, runnableRun)
     }
 
     @Test
@@ -1351,10 +1393,7 @@
         forceLayout()
         val removedView = rootView.getChildAt(0)
 
-        ViewHierarchyAnimator.animateRemoval(
-            removedView,
-            onAnimationEnd = onAnimationEndRunnable
-        )
+        ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable)
         advanceAnimation(removedView, 0.5f)
 
         assertEquals(false, runnableRun)
@@ -1370,7 +1409,7 @@
         rootView.addView(secondChild)
         rootView.measure(
             View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
-            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
         )
         rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */)
 
@@ -1378,7 +1417,7 @@
         // Change all bounds.
         rootView.measure(
             View.MeasureSpec.makeMeasureSpec(150, View.MeasureSpec.EXACTLY),
-            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+            View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY),
         )
         rootView.layout(0 /* l */, 0 /* t */, 150 /* r */, 100 /* b */)
 
@@ -1501,7 +1540,7 @@
         checkBounds(rootView, l = 0, t = 15, r = 70, b = 80)
 
         // Change all bounds again.
-        rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)
+        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
 
         assertNull(rootView.getTag(R.id.tag_animator))
         checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
@@ -1523,7 +1562,7 @@
 
         ViewHierarchyAnimator.stopAnimating(rootView)
         // Change all bounds again.
-        rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */)
+        rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
 
         assertNull(rootView.getTag(R.id.tag_animator))
         checkBounds(rootView, l = 10, t = 10, r = 50, b = 50)
@@ -1543,10 +1582,8 @@
         val secondChild = View(mContext)
         rootView.addView(secondChild)
 
-        val firstChildParams = LinearLayout.LayoutParams(
-            0 /* width */,
-            LinearLayout.LayoutParams.MATCH_PARENT
-        )
+        val firstChildParams =
+            LinearLayout.LayoutParams(0 /* width */, LinearLayout.LayoutParams.MATCH_PARENT)
         firstChildParams.weight = 0.5f
         if (includeMarginsOnFirstChild) {
             firstChildParams.leftMargin = M_LEFT
@@ -1556,23 +1593,25 @@
         }
         firstChild.layoutParams = firstChildParams
 
-        val secondChildParams = LinearLayout.LayoutParams(
-            0 /* width */,
-            LinearLayout.LayoutParams.MATCH_PARENT
-        )
+        val secondChildParams =
+            LinearLayout.LayoutParams(0 /* width */, LinearLayout.LayoutParams.MATCH_PARENT)
         secondChildParams.weight = 0.5f
         secondChild.layoutParams = secondChildParams
 
         firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
-        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
-            .addRule(RelativeLayout.ALIGN_PARENT_START)
-        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams)
-            .addRule(RelativeLayout.ALIGN_PARENT_TOP)
+        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule(
+            RelativeLayout.ALIGN_PARENT_START
+        )
+        (firstGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule(
+            RelativeLayout.ALIGN_PARENT_TOP
+        )
         secondGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */)
-        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
-            .addRule(RelativeLayout.ALIGN_PARENT_END)
-        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams)
-            .addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
+        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule(
+            RelativeLayout.ALIGN_PARENT_END
+        )
+        (secondGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule(
+            RelativeLayout.ALIGN_PARENT_BOTTOM
+        )
 
         forceLayout()
     }
@@ -1580,7 +1619,7 @@
     private fun forceLayout() {
         rootView.measure(
             View.MeasureSpec.makeMeasureSpec(200 /* width */, View.MeasureSpec.AT_MOST),
-            View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST)
+            View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST),
         )
         rootView.layout(0 /* l */, 0 /* t */, 200 /* r */, 100 /* b */)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 160865d..f0d79bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.deviceentry.domain.interactor
 
 import android.content.pm.UserInfo
+import android.os.PowerManager
+import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
@@ -31,22 +33,26 @@
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
-import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -228,6 +234,8 @@
     @Test
     fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep() =
         testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
 
             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
@@ -243,16 +251,55 @@
         }
 
     @Test
-    fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleepInAod() =
+    fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_afterDelay() =
         testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            kosmos.powerInteractor.setAsleepForTest()
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            advanceTimeBy(delay.toLong())
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_powerButtonLocksInstantly() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(5000)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = true
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            kosmos.powerInteractor.setAsleepForTest(
+                sleepReason = PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON
+            )
+            runCurrent()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleep() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(0)
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
             assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
 
-            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.AOD,
-                testScope = this,
-            )
             kosmos.powerInteractor.setAsleepForTest()
             runCurrent()
 
@@ -266,26 +313,6 @@
         }
 
     @Test
-    fun deviceUnlockStatus_staysLocked_whenFingerprintUnlocked_whileDeviceAsleep() =
-        testScope.runTest {
-            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
-            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
-            assertThat(kosmos.keyguardTransitionInteractor.getCurrentState())
-                .isEqualTo(KeyguardState.LOCKSCREEN)
-
-            kosmos.powerInteractor.setAsleepForTest()
-            runCurrent()
-
-            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
-
-            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
-                SuccessFingerprintAuthenticationStatus(0, true)
-            )
-            runCurrent()
-            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
-        }
-
-    @Test
     fun deviceEntryRestrictionReason_whenFaceOrFingerprintOrTrust_alwaysNull() =
         testScope.runTest {
             kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
@@ -478,6 +505,98 @@
                 .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
         }
 
+    @Test
+    fun deviceUnlockStatus_locksImmediately_whenDreamStarts_noTimeout() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_locksWithDelay_afterDreamStarts_withTimeout() =
+        testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(delay - 1L)
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(1L)
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_doesNotLockWithDelay_whenDreamStopsBeforeTimeout() =
+        testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(delay - 1L)
+            assertThat(isUnlocked).isTrue()
+
+            stopDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(1L)
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun deviceUnlockStatus_doesNotLock_whenDreamStarts_ifNotInteractive() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    private fun TestScope.unlockDevice() {
+        val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+        kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
+        runCurrent()
+    }
+
+    private fun setLockAfterScreenTimeout(timeoutMs: Int) {
+        kosmos.fakeSettings.putIntForUser(
+            Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+            timeoutMs,
+            kosmos.selectedUserInteractor.getSelectedUserId(),
+        )
+    }
+
+    private fun TestScope.startDreaming() {
+        kosmos.fakeKeyguardRepository.setDreaming(true)
+        runCurrent()
+    }
+
+    private fun TestScope.stopDreaming() {
+        kosmos.fakeKeyguardRepository.setDreaming(false)
+        runCurrent()
+    }
+
     private fun TestScope.verifyRestrictionReasonsForAuthFlags(
         vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?>
     ) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index bfe89de..3d5498b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.animation.Animator
 import android.animation.ValueAnimator
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.app.animation.Interpolators
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -41,6 +44,8 @@
 import java.math.BigDecimal
 import java.math.RoundingMode
 import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.dropWhile
@@ -53,6 +58,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -65,6 +71,8 @@
     private lateinit var underTest: KeyguardTransitionRepository
     private lateinit var runner: KeyguardTransitionRunner
 
+    private val animatorListener = mock<Animator.AnimatorListener>()
+
     @Before
     fun setUp() {
         underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main)
@@ -80,7 +88,7 @@
             runner.startTransition(
                 this,
                 TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
-                maxFrames = 100
+                maxFrames = 100,
             )
 
             assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
@@ -107,7 +115,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.LAST_VALUE
+                    TransitionModeOnCanceled.LAST_VALUE,
                 ),
             )
 
@@ -142,7 +150,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.RESET
+                    TransitionModeOnCanceled.RESET,
                 ),
             )
 
@@ -177,7 +185,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.REVERSE
+                    TransitionModeOnCanceled.REVERSE,
                 ),
             )
 
@@ -476,6 +484,49 @@
             assertThat(steps.size).isEqualTo(3)
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
+    fun forceFinishCurrentTransition_noFurtherStepsEmitted() =
+        testScope.runTest {
+            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
+
+            var sentForceFinish = false
+
+            runner.startTransition(
+                this,
+                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
+                maxFrames = 100,
+                // Force-finish on the second frame.
+                frameCallback = { frameNumber ->
+                    if (!sentForceFinish && frameNumber > 1) {
+                        testScope.launch { underTest.forceFinishCurrentTransition() }
+                        sentForceFinish = true
+                    }
+                },
+            )
+
+            val lastTwoRunningSteps =
+                steps.filter { it.transitionState == TransitionState.RUNNING }.takeLast(2)
+
+            // Make sure we stopped emitting RUNNING steps early, but then emitted a final 1f step.
+            assertTrue(lastTwoRunningSteps[0].value < 0.5f)
+            assertTrue(lastTwoRunningSteps[1].value == 1f)
+
+            assertEquals(steps.last().from, AOD)
+            assertEquals(steps.last().to, LOCKSCREEN)
+            assertEquals(steps.last().transitionState, TransitionState.FINISHED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
+    fun forceFinishCurrentTransition_noTransitionStarted_noStepsEmitted() =
+        testScope.runTest {
+            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
+
+            underTest.forceFinishCurrentTransition()
+            assertEquals(0, steps.size)
+        }
+
     private fun listWithStep(
         step: BigDecimal,
         start: BigDecimal = BigDecimal.ZERO,
@@ -505,7 +556,7 @@
                     to,
                     fractions[0].toFloat(),
                     TransitionState.STARTED,
-                    OWNER_NAME
+                    OWNER_NAME,
                 )
             )
         fractions.forEachIndexed { index, fraction ->
@@ -519,7 +570,7 @@
                         to,
                         fraction.toFloat(),
                         TransitionState.RUNNING,
-                        OWNER_NAME
+                        OWNER_NAME,
                     )
                 )
         }
@@ -538,6 +589,7 @@
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
             setDuration(10)
+            addListener(animatorListener)
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
index 8914c80..ae2a5c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
@@ -34,6 +34,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -43,6 +44,7 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -60,10 +62,13 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -96,7 +101,7 @@
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -134,7 +139,7 @@
                 from = KeyguardState.AOD,
                 to = KeyguardState.LOCKSCREEN,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -182,21 +187,12 @@
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
             runCurrent()
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                    true,
-                )
+            assertThat(values).containsExactly(false, true)
 
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false)
             runCurrent()
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                    true,
-                    false,
-                )
+            assertThat(values).containsExactly(false, true, false)
 
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
             runCurrent()
@@ -228,7 +224,7 @@
                 from = KeyguardState.GONE,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -242,10 +238,7 @@
                 testScope = testScope,
             )
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                )
+            assertThat(values).containsExactly(false)
         }
 
     @Test
@@ -263,7 +256,7 @@
                 from = KeyguardState.UNDEFINED,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -278,10 +271,7 @@
                 testScope = testScope,
             )
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                )
+            assertThat(values).containsExactly(false)
         }
 
     @Test
@@ -304,8 +294,19 @@
             assertThat(occludingActivityWillDismissKeyguard).isTrue()
 
             // Re-lock device:
-            kosmos.powerInteractor.setAsleepForTest()
-            runCurrent()
+            lockDevice()
             assertThat(occludingActivityWillDismissKeyguard).isFalse()
         }
+
+    private suspend fun TestScope.lockDevice() {
+        kosmos.powerInteractor.setAsleepForTest()
+        advanceTimeBy(
+            kosmos.userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        )
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
index 1abb441..5798e07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -21,6 +21,7 @@
 import android.view.Choreographer.FrameCallback
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import java.util.function.Consumer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -35,9 +36,8 @@
  * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See
  * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly.
  */
-class KeyguardTransitionRunner(
-    val repository: KeyguardTransitionRepository,
-) : AnimationFrameCallbackProvider {
+class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) :
+    AnimationFrameCallbackProvider {
 
     private var frameCount = 1L
     private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
@@ -48,7 +48,12 @@
      * For transitions being directed by an animator. Will control the number of frames being
      * generated so the values are deterministic.
      */
-    suspend fun startTransition(scope: CoroutineScope, info: TransitionInfo, maxFrames: Int = 100) {
+    suspend fun startTransition(
+        scope: CoroutineScope,
+        info: TransitionInfo,
+        maxFrames: Int = 100,
+        frameCallback: Consumer<Long>? = null,
+    ) {
         // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main
         // thread
         withContext(Dispatchers.Main) {
@@ -62,7 +67,12 @@
 
                     isTerminated = frameNumber >= maxFrames
                     if (!isTerminated) {
-                        withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
+                        try {
+                            withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
+                            frameCallback?.accept(frameNumber)
+                        } catch (e: IllegalStateException) {
+                            e.printStackTrace()
+                        }
                     }
                 }
             }
@@ -90,9 +100,13 @@
     override fun postFrameCallback(cb: FrameCallback) {
         frames.value = Pair(frameCount++, cb)
     }
+
     override fun postCommitCallback(runnable: Runnable) {}
+
     override fun getFrameTime() = frameCount
+
     override fun getFrameDelay() = 1L
+
     override fun setFrameDelay(delay: Long) {}
 
     companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
index 8e67e60..f8f6fe2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.media.controls.util.fakeSessionTokenFactory
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
+import com.android.systemui.util.concurrency.execution
 import com.google.common.collect.ImmutableList
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runCurrent
@@ -105,6 +106,7 @@
                 kosmos.looper,
                 handler,
                 kosmos.testScope,
+                kosmos.execution,
             )
 
         controllerFactory.setMedia3Controller(media3Controller)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
index 8ccaf6b..0f63150 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
@@ -300,7 +300,7 @@
         mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "0",
                 UserHandle.USER_CURRENT);
         verifyActivityDetails("abc/.def");
-        assertThat(mController.isEnabledForLockScreenButton()).isFalse();
+        assertThat(mController.isEnabledForLockScreenButton()).isTrue();
         assertThat(mController.isAllowedOnLockScreen()).isTrue();
         assertThat(mController.isAbleToLaunchScannerActivity()).isTrue();
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 5cba325..03feceb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -318,6 +318,63 @@
             }
         }
 
+    @Test
+    fun qqsMediaExpansion_collapsedMediaInLandscape() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                setCollapsedMediaInLandscape(true)
+                setMediaState(ACTIVE_MEDIA)
+
+                setConfigurationForMediaInRow(mediaInRow = false)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+
+                setConfigurationForMediaInRow(mediaInRow = true)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.COLLAPSED)
+            }
+        }
+
+    @Test
+    fun qqsMediaExpansion_notCollapsedMediaInLandscape_alwaysExpanded() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                setCollapsedMediaInLandscape(false)
+                setMediaState(ACTIVE_MEDIA)
+
+                setConfigurationForMediaInRow(mediaInRow = false)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+
+                setConfigurationForMediaInRow(mediaInRow = true)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+            }
+        }
+
+    @Test
+    fun qqsMediaExpansion_reactsToChangesInCollapsedMediaInLandscape() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                setConfigurationForMediaInRow(mediaInRow = true)
+                setMediaState(ACTIVE_MEDIA)
+
+                setCollapsedMediaInLandscape(false)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED)
+
+                setCollapsedMediaInLandscape(true)
+                Snapshot.sendApplyNotifications()
+                runCurrent()
+                assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.COLLAPSED)
+            }
+        }
+
     private fun TestScope.setMediaState(state: MediaState) {
         with(kosmos) {
             val activeMedia = state == ACTIVE_MEDIA
@@ -331,6 +388,14 @@
         runCurrent()
     }
 
+    private fun TestScope.setCollapsedMediaInLandscape(collapsed: Boolean) {
+        with(kosmos) {
+            overrideResource(R.bool.config_quickSettingsMediaLandscapeCollapsed, collapsed)
+            fakeConfigurationRepository.onAnyConfigurationChange()
+        }
+        runCurrent()
+    }
+
     companion object {
         private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
index dda9cd5..4dcbdfa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
@@ -104,67 +104,6 @@
             }
         }
 
-    @Test
-    fun showLabels_updatesFromSharedPreferences() =
-        with(kosmos) {
-            testScope.runTest {
-                val latest by collectLastValue(underTest.showLabels)
-                assertThat(latest).isFalse()
-
-                setShowLabelsInSharedPreferences(true)
-                assertThat(latest).isTrue()
-
-                setShowLabelsInSharedPreferences(false)
-                assertThat(latest).isFalse()
-            }
-        }
-
-    @Test
-    fun showLabels_updatesFromUserChange() =
-        with(kosmos) {
-            testScope.runTest {
-                fakeUserRepository.setUserInfos(USERS)
-                val latest by collectLastValue(underTest.showLabels)
-
-                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
-                setShowLabelsInSharedPreferences(false)
-
-                fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
-                setShowLabelsInSharedPreferences(true)
-
-                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
-                assertThat(latest).isFalse()
-            }
-        }
-
-    @Test
-    fun setShowLabels_inSharedPreferences() {
-        underTest.setShowLabels(false)
-        assertThat(getShowLabelsFromSharedPreferences(true)).isFalse()
-
-        underTest.setShowLabels(true)
-        assertThat(getShowLabelsFromSharedPreferences(false)).isTrue()
-    }
-
-    @Test
-    fun setShowLabels_forDifferentUser() =
-        with(kosmos) {
-            testScope.runTest {
-                fakeUserRepository.setUserInfos(USERS)
-
-                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
-                underTest.setShowLabels(false)
-                assertThat(getShowLabelsFromSharedPreferences(true)).isFalse()
-
-                fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
-                underTest.setShowLabels(true)
-                assertThat(getShowLabelsFromSharedPreferences(false)).isTrue()
-
-                fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
-                assertThat(getShowLabelsFromSharedPreferences(true)).isFalse()
-            }
-        }
-
     private fun getSharedPreferences(): SharedPreferences =
         with(kosmos) {
             return userFileManager.getSharedPreferences(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
index 0729e2f..03c1f92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
@@ -93,6 +93,8 @@
     private static final String CARD_DESCRIPTION = "•••• 1234";
     private static final Icon CARD_IMAGE =
             Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888));
+    private static final Icon INVALID_CARD_IMAGE =
+            Icon.createWithContentUri("content://media/external/images/media");
     private static final int PRIMARY_USER_ID = 0;
     private static final int SECONDARY_USER_ID = 10;
 
@@ -444,6 +446,14 @@
     }
 
     @Test
+    public void testQueryCards_invalidDrawable_noSideViewDrawable() {
+        when(mKeyguardStateController.isUnlocked()).thenReturn(true);
+        setUpInvalidWalletCard(/* hasCard= */ true);
+
+        assertNull(mTile.getState().sideViewCustomDrawable);
+    }
+
+    @Test
     public void testQueryCards_error_notUpdateSideViewDrawable() {
         String errorMessage = "getWalletCardsError";
         GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage);
@@ -503,9 +513,33 @@
         mTestableLooper.processAllMessages();
     }
 
+    private void setUpInvalidWalletCard(boolean hasCard) {
+        GetWalletCardsResponse response =
+                new GetWalletCardsResponse(
+                        hasCard
+                                ? Collections.singletonList(createInvalidWalletCard(mContext))
+                                : Collections.EMPTY_LIST, 0);
+
+        mTile.handleSetListening(true);
+
+        verify(mController).queryWalletCards(mCallbackCaptor.capture());
+
+        mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
+        mTestableLooper.processAllMessages();
+    }
+
     private WalletCard createWalletCard(Context context) {
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
         return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
     }
+
+    private WalletCard createInvalidWalletCard(Context context) {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
+        return new WalletCard.Builder(
+                CARD_ID, INVALID_CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
+    }
+
+
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 3be8a38..b5f005c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.scene
 
+import android.provider.Settings
 import android.telephony.TelephonyManager
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,6 +43,7 @@
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
@@ -64,6 +66,7 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.android.telecom.mockTelecomManager
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -72,6 +75,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import org.junit.Before
 import org.junit.Test
@@ -541,7 +545,14 @@
             .isTrue()
 
         powerInteractor.setAsleepForTest()
-        testScope.runCurrent()
+        testScope.advanceTimeBy(
+            kosmos.userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        )
 
         powerInteractor.setAwakeForTest()
         testScope.runCurrent()
@@ -631,14 +642,25 @@
     }
 
     /** Changes device wakefulness state from awake to asleep, going through intermediary states. */
-    private fun Kosmos.putDeviceToSleep() {
+    private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) {
         val wakefulnessModel = powerInteractor.detailedWakefulness.value
         assertWithMessage("Cannot put device to sleep as it's already asleep!")
             .that(wakefulnessModel.isAwake())
             .isTrue()
 
         powerInteractor.setAsleepForTest()
-        testScope.runCurrent()
+        if (waitForLock) {
+            testScope.advanceTimeBy(
+                kosmos.userAwareSecureSettingsRepository
+                    .getInt(
+                        Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                        KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                    )
+                    .toLong()
+            )
+        } else {
+            testScope.runCurrent()
+        }
     }
 
     /** Emulates the dismissal of the IME (soft keyboard). */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 55f88cc..152911a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -23,6 +23,7 @@
 import android.os.PowerManager
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
 import android.view.Display
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -59,6 +60,7 @@
 import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -106,6 +108,7 @@
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -753,7 +756,7 @@
                 lastSleepReason = WakeSleepReason.POWER_BUTTON,
                 powerButtonLaunchGestureTriggered = false,
             )
-            transitionStateFlow.value = Transition(from = Scenes.Shade, to = Scenes.Lockscreen)
+            transitionStateFlow.value = Transition(from = Scenes.Gone, to = Scenes.Lockscreen)
             assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
 
             kosmos.fakePowerRepository.updateWakefulness(
@@ -1339,7 +1342,14 @@
             // Putting the device to sleep to lock it again, which shouldn't report another
             // successful unlock.
             kosmos.powerInteractor.setAsleepForTest()
-            runCurrent()
+            advanceTimeBy(
+                kosmos.userAwareSecureSettingsRepository
+                    .getInt(
+                        Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                        KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                    )
+                    .toLong()
+            )
             // Verify that the startable changed the scene to Lockscreen because the device locked
             // following the sleep.
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt
new file mode 100644
index 0000000..a9a5cac
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePositionRepositoryTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.data.repository
+
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.commandline.commandRegistry
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadePositionRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val commandRegistry = kosmos.commandRegistry
+    private val pw = PrintWriter(StringWriter())
+
+    private val underTest = ShadePositionRepositoryImpl(commandRegistry)
+
+    @Before
+    fun setUp() {
+        underTest.start()
+    }
+
+    @Test
+    fun commandDisplayOverride_updatesDisplayId() =
+        testScope.runTest {
+            val displayId by collectLastValue(underTest.displayId)
+            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+            val newDisplayId = 2
+            commandRegistry.onShellCommand(
+                pw,
+                arrayOf("shade_display_override", newDisplayId.toString()),
+            )
+
+            assertThat(displayId).isEqualTo(newDisplayId)
+        }
+
+    @Test
+    fun commandShadeDisplayOverride_resetsDisplayId() =
+        testScope.runTest {
+            val displayId by collectLastValue(underTest.displayId)
+            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+            val newDisplayId = 2
+            commandRegistry.onShellCommand(
+                pw,
+                arrayOf("shade_display_override", newDisplayId.toString()),
+            )
+            assertThat(displayId).isEqualTo(newDisplayId)
+
+            commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "reset"))
+            assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 0efd591..11a125a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -16,29 +16,35 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 class ScreenRecordChipInteractorTest : SysuiTestCase() {
-    private val kosmos = Kosmos().also { it.testCase = this }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val screenRecordRepo = kosmos.screenRecordRepository
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
@@ -116,6 +122,137 @@
         }
 
     @Test
+    @DisableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_flagOff_doesNotAutomaticallySwitchToRecordingBasedOnTime() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            // WHEN screen record should start in 900ms
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900))
+
+            // WHEN 900ms has elapsed
+            advanceTimeBy(901)
+
+            // THEN we don't automatically update to the recording state if the flag is off
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900))
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_flagOn_automaticallySwitchesToRecordingBasedOnTime() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            // WHEN screen record should start in 900ms
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900))
+
+            // WHEN 900ms has elapsed
+            advanceTimeBy(901)
+
+            // THEN we automatically update to the recording state
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_recordingBeginsEarly_switchesToRecording() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            // WHEN screen record should start in 900ms
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900))
+
+            // WHEN we update to the Recording state earlier than 900ms
+            advanceTimeBy(800)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+            val task = createTask(taskId = 1)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    "host.package",
+                    hostDeviceName = null,
+                    task,
+                )
+
+            // THEN we immediately switch to Recording, and we have the task
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
+
+            // WHEN more than 900ms has elapsed
+            advanceTimeBy(200)
+
+            // THEN we still stay in the Recording state and we have the task
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task))
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_secondRecording_doesNotAutomaticallyStart() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            // First recording starts, records, and stops
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900)
+            advanceTimeBy(900)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+            advanceTimeBy(5000)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
+            advanceTimeBy(10000)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing)
+
+            // WHEN a second recording is starting
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900)
+
+            // THEN we stay as starting and do not switch to Recording (verifying the auto-start
+            // timer is reset)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900))
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_startingButThenDoingNothing_doesNotAutomaticallyStart() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            // WHEN a screen recording is starting in 500ms
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500))
+
+            // But it's cancelled after 300ms
+            advanceTimeBy(300)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            // THEN we don't automatically start the recording 200ms later
+            advanceTimeBy(201)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing)
+        }
+
+    @Test
+    @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP)
+    fun screenRecordState_multipleStartingValues_autoStartResets() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.screenRecordState)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900))
+
+            advanceTimeBy(2800)
+
+            // WHEN there's 100ms left to go before auto-start, but then we get a new start time
+            // that's in 500ms
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500)
+
+            // THEN we don't auto-start in 100ms
+            advanceTimeBy(101)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500))
+
+            // THEN we *do* auto-start 400ms later
+            advanceTimeBy(401)
+            assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null))
+        }
+
+    @Test
     fun stopRecording_sendsToRepo() =
         testScope.runTest {
             assertThat(screenRecordRepo.stopRecordingInvoked).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index bfebe18..48d8add6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -26,9 +26,8 @@
 import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager
@@ -44,6 +43,7 @@
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
@@ -61,7 +61,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ScreenRecordChipViewModelTest : SysuiTestCase() {
-    private val kosmos = Kosmos().also { it.testCase = this }
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
     private val testScope = kosmos.testScope
     private val screenRecordRepo = kosmos.screenRecordRepository
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
@@ -254,7 +254,7 @@
                 MediaProjectionState.Projecting.SingleTask(
                     "host.package",
                     hostDeviceName = null,
-                    FakeActivityTaskManager.createTask(taskId = 1)
+                    FakeActivityTaskManager.createTask(taskId = 1),
                 )
 
             // THEN the start time is still the old start time
@@ -275,12 +275,7 @@
             clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    any(),
-                    anyBoolean(),
-                )
+                .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean())
         }
 
     @Test
@@ -297,12 +292,7 @@
             clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    any(),
-                    anyBoolean(),
-                )
+                .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean())
         }
 
     @Test
@@ -314,7 +304,7 @@
                 MediaProjectionState.Projecting.SingleTask(
                     "host.package",
                     hostDeviceName = null,
-                    FakeActivityTaskManager.createTask(taskId = 1)
+                    FakeActivityTaskManager.createTask(taskId = 1),
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
@@ -323,12 +313,7 @@
             clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    eq(mockSystemUIDialog),
-                    eq(chipBackgroundView),
-                    any(),
-                    anyBoolean(),
-                )
+                .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean())
         }
 
     @Test
@@ -344,12 +329,7 @@
 
             val cujCaptor = argumentCaptor<DialogCuj>()
             verify(kosmos.mockDialogTransitionAnimator)
-                .showFromView(
-                    any(),
-                    any(),
-                    cujCaptor.capture(),
-                    anyBoolean(),
-                )
+                .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
 
             assertThat(cujCaptor.firstValue.cujType)
                 .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index e96def6..c5c2a94 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
@@ -48,6 +47,7 @@
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
+import com.android.systemui.testKosmos
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -72,7 +72,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @DisableFlags(StatusBarNotifChips.FLAG_NAME)
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
-    private val kosmos = Kosmos().also { it.testCase = this }
+    private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val systemClock = kosmos.fakeSystemClock
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
index 643acdb..2a3878c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt
@@ -16,18 +16,22 @@
 
 package com.android.systemui.statusbar.connectivity
 
+import android.content.Context
+import android.os.UserHandle
 import android.os.UserManager
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper.RunWithLooper
 import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.wifitrackerlib.WifiEntry
 import com.android.wifitrackerlib.WifiPickerTracker
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,36 +39,28 @@
 import org.mockito.ArgumentMatchers.anyList
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
 class AccessPointControllerImplTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var userManager: UserManager
-    @Mock
-    private lateinit var userTracker: UserTracker
-    @Mock
-    private lateinit var wifiPickerTrackerFactory:
-            WifiPickerTrackerFactory
-    @Mock
-    private lateinit var wifiPickerTracker: WifiPickerTracker
-    @Mock
-    private lateinit var callback: AccessPointController.AccessPointCallback
-    @Mock
-    private lateinit var otherCallback: AccessPointController.AccessPointCallback
-    @Mock
-    private lateinit var wifiEntryConnected: WifiEntry
-    @Mock
-    private lateinit var wifiEntryOther: WifiEntry
-    @Captor
-    private lateinit var wifiEntryListCaptor: ArgumentCaptor<List<WifiEntry>>
+    @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory
+    @Mock private lateinit var wifiPickerTracker: WifiPickerTracker
+    @Mock private lateinit var callback: AccessPointController.AccessPointCallback
+    @Mock private lateinit var otherCallback: AccessPointController.AccessPointCallback
+    @Mock private lateinit var wifiEntryConnected: WifiEntry
+    @Mock private lateinit var wifiEntryOther: WifiEntry
+    @Captor private lateinit var wifiEntryListCaptor: ArgumentCaptor<List<WifiEntry>>
 
     private val instantExecutor = Executor { it.run() }
     private lateinit var controller: AccessPointControllerImpl
@@ -72,19 +68,21 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        `when`(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(wifiPickerTracker)
+        `when`(wifiPickerTrackerFactory.create(any(), any(), any(), any()))
+            .thenReturn(wifiPickerTracker)
 
         `when`(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntryConnected)
-        `when`(wifiPickerTracker.wifiEntries).thenReturn(ArrayList<WifiEntry>().apply {
-            add(wifiEntryOther)
-        })
+        `when`(wifiPickerTracker.wifiEntries)
+            .thenReturn(ArrayList<WifiEntry>().apply { add(wifiEntryOther) })
 
-        controller = AccessPointControllerImpl(
+        controller =
+            AccessPointControllerImpl(
+                mContext,
                 userManager,
                 userTracker,
                 instantExecutor,
-                wifiPickerTrackerFactory
-        )
+                wifiPickerTrackerFactory,
+            )
 
         controller.init()
     }
@@ -183,13 +181,15 @@
 
     @Test
     fun testReturnEmptyListWhenNoWifiPickerTracker() {
-        `when`(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(null)
-        val otherController = AccessPointControllerImpl(
+        `when`(wifiPickerTrackerFactory.create(any(), any(), any(), any())).thenReturn(null)
+        val otherController =
+            AccessPointControllerImpl(
+                mContext,
                 userManager,
                 userTracker,
                 instantExecutor,
-                wifiPickerTrackerFactory
-        )
+                wifiPickerTrackerFactory,
+            )
         otherController.init()
 
         otherController.addAccessPointCallback(callback)
@@ -244,4 +244,19 @@
         verify(wifiEntryOther).connect(any())
         verify(callback, never()).onSettingsActivityTriggered(any())
     }
+
+    @Test
+    @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT)
+    fun switchUsers() {
+        val primaryUserMockContext = mock<Context>()
+        mContext.prepareCreateContextAsUser(UserHandle.of(PRIMARY_USER_ID), primaryUserMockContext)
+        controller.onUserSwitched(PRIMARY_USER_ID)
+        // Create is expected to be called once when the test starts and a second time when the user
+        // is switched.
+        verify(wifiPickerTrackerFactory, times(2)).create(any(), any(), any(), any())
+    }
+
+    private companion object {
+        private const val PRIMARY_USER_ID = 1
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt
index 938da88..009b33b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarInitializerTest.kt
@@ -23,7 +23,6 @@
 import android.view.ViewGroup
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.fragments.FragmentHostManager
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
@@ -81,21 +80,21 @@
         )
 
     @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    fun simpleFragment_startsFromCoreStartable() {
+    @EnableFlags(StatusBarRootModernization.FLAG_NAME)
+    fun flagOn_startsFromCoreStartable() {
         underTest.start()
         assertThat(underTest.initialized).isTrue()
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    fun simpleFragment_throwsIfInitializeIsCalled() {
+    @EnableFlags(StatusBarRootModernization.FLAG_NAME)
+    fun flagOn_throwsIfInitializeIsCalled() {
         assertThrows(IllegalStateException::class.java) { underTest.initializeStatusBar() }
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
-    fun simpleFragment_flagEnabled_doesNotCreateFragment() {
+    @EnableFlags(StatusBarRootModernization.FLAG_NAME)
+    fun flagOn_flagEnabled_doesNotCreateFragment() {
         underTest.start()
 
         verify(fragmentManager, never()).beginTransaction()
@@ -103,14 +102,14 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     fun flagOff_startCalled_stillInitializes() {
         underTest.start()
         assertThat(underTest.initialized).isTrue()
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     fun flagOff_doesNotThrowIfInitializeIsCalled() {
         underTest.initializeStatusBar()
         assertThat(underTest.initialized).isTrue()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 3b5d358..c4b1b84 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.statusbar.notification
 
+import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
@@ -26,11 +26,17 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.data.repository.Idle
+import com.android.systemui.scene.data.repository.setSceneTransition
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ShadeViewController.Companion.WAKEUP_ANIMATION_DELAY_MS
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -41,9 +47,6 @@
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,16 +58,21 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.anyFloat
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class NotificationWakeUpCoordinatorTest : SysuiTestCase() {
+class NotificationWakeUpCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
 
@@ -105,6 +113,18 @@
         statusBarStateCallback.onDozeAmountChanged(dozeAmount, dozeAmount)
     }
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
     @Before
     fun setup() {
         whenever(bypassController.bypassEnabled).then { bypassEnabled }
@@ -178,6 +198,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun setDozeToZeroWhenCommunalShowingWillFullyHideNotifications() =
         testScope.runTest {
             val transitionState =
@@ -192,6 +213,17 @@
         }
 
     @Test
+    @EnableSceneContainer
+    fun setDozeToZeroWhenCommunalShowingWillFullyHideNotifications_withSceneContainer() =
+        testScope.runTest {
+            kosmos.setSceneTransition(Idle(Scenes.Communal))
+            setDozeAmount(0f)
+            verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f)
+            assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue()
+        }
+
+    @Test
+    @DisableSceneContainer
     fun closingCommunalWillShowNotifications() =
         testScope.runTest {
             val transitionState =
@@ -211,6 +243,20 @@
         }
 
     @Test
+    @EnableSceneContainer
+    fun closingCommunalWillShowNotifications_withSceneContainer() =
+        testScope.runTest {
+            kosmos.setSceneTransition(Idle(Scenes.Communal))
+            setDozeAmount(0f)
+            verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f)
+            assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue()
+
+            kosmos.setSceneTransition(Idle(CommunalScenes.Blank))
+            verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f)
+            assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse()
+        }
+
+    @Test
     fun switchingToShadeWithBypassEnabledWillShowNotifications() {
         setDozeToZeroWithBypassWillFullyHideNotifications()
         clearInvocations(stackScrollerController)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
index ea5c29e..3ad41a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
+import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertFalse;
@@ -32,9 +34,9 @@
 import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
 
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.compose.animation.scene.ObservableTransitionState;
@@ -42,6 +44,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.communal.shared.model.CommunalScenes;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.BrokenWithSceneContainer;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -78,14 +81,23 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.verification.VerificationMode;
 
+import java.util.List;
+
 import kotlinx.coroutines.flow.MutableStateFlow;
 import kotlinx.coroutines.test.TestScope;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper
 public class VisualStabilityCoordinatorTest extends SysuiTestCase {
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return parameterizeSceneContainerFlag();
+    }
+
     private VisualStabilityCoordinator mCoordinator;
 
     @Mock private DumpManager mDumpManager;
@@ -117,6 +129,11 @@
     private NotificationEntry mEntry;
     private GroupEntry mGroupEntry;
 
+    public VisualStabilityCoordinatorTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -251,6 +268,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() {
         // GIVEN the panel true expanded and device isn't pulsing
         setFullyDozed(false);
@@ -267,6 +285,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() {
         // GIVEN the panel true expanded and device isn't pulsing
         setFullyDozed(false);
@@ -520,6 +539,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION)
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testNotLockscreenInGoneTransition_invalidationCalled() {
         // GIVEN visual stability is being maintained b/c animation is playing
         mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava(
@@ -589,6 +609,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testCommunalShowingWillNotSuppressReordering() {
         // GIVEN panel is expanded, communal is showing, and QS is collapsed
         setPulsing(false);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
index dae5542..50db9f7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt
@@ -21,16 +21,19 @@
 import android.view.View.VISIBLE
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.nullable
@@ -52,8 +55,11 @@
     private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var stackLayout: NotificationStackScrollLayout
+    @Mock private lateinit var seenNotificationsInteractor: SeenNotificationsInteractor
 
     private val testableResources = mContext.orCreateTestableResources
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     private lateinit var sizeCalculator: NotificationStackSizeCalculator
 
@@ -72,7 +78,9 @@
                 lockscreenShadeTransitionController = lockscreenShadeTransitionController,
                 mediaDataManager = mediaDataManager,
                 testableResources.resources,
-                    ResourcesSplitShadeStateController()
+                ResourcesSplitShadeStateController(),
+                seenNotificationsInteractor = seenNotificationsInteractor,
+                scope = testScope,
             )
     }
 
@@ -85,7 +93,7 @@
                 rows,
                 spaceForNotifications = 0f,
                 spaceForShelf = 0f,
-                shelfHeight = 0f
+                shelfHeight = 0f,
             )
 
         assertThat(maxNotifications).isEqualTo(0)
@@ -101,7 +109,7 @@
                 rows,
                 spaceForNotifications = Float.MAX_VALUE,
                 spaceForShelf = Float.MAX_VALUE,
-                shelfHeight
+                shelfHeight,
             )
 
         assertThat(maxNotifications).isEqualTo(numberOfRows)
@@ -137,7 +145,7 @@
                 listOf(row),
                 /* spaceForNotifications= */ 5f,
                 /* spaceForShelf= */ 0f,
-                /* shelfHeight= */ 0f
+                /* shelfHeight= */ 0f,
             )
 
         assertThat(maxNotifications).isEqualTo(1)
@@ -148,11 +156,7 @@
         setGapHeight(gapHeight)
         val shelfHeight = shelfHeight + dividerHeight
         val spaceForNotifications =
-            listOf(
-                    rowHeight + dividerHeight,
-                    gapHeight + rowHeight + dividerHeight,
-                )
-                .sum()
+            listOf(rowHeight + dividerHeight, gapHeight + rowHeight + dividerHeight).sum()
         val spaceForShelf = gapHeight + dividerHeight + shelfHeight
         val rows =
             listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight))
@@ -162,7 +166,7 @@
                 rows,
                 spaceForNotifications + 1,
                 spaceForShelf,
-                shelfHeight
+                shelfHeight,
             )
 
         assertThat(maxNotifications).isEqualTo(2)
@@ -173,12 +177,7 @@
         // Each row in separate section.
         setGapHeight(gapHeight)
 
-        val notifSpace =
-            listOf(
-                    rowHeight,
-                    dividerHeight + gapHeight + rowHeight,
-                )
-                .sum()
+        val notifSpace = listOf(rowHeight, dividerHeight + gapHeight + rowHeight).sum()
 
         val shelfSpace = dividerHeight + gapHeight + shelfHeight
         val spaceUsed = notifSpace + shelfSpace
@@ -209,7 +208,7 @@
                 rows,
                 spaceForNotifications + 1,
                 spaceForShelf,
-                shelfHeight
+                shelfHeight,
             )
         assertThat(maxNotifications).isEqualTo(1)
 
@@ -252,7 +251,7 @@
                 visibleIndex = 0,
                 previousView = null,
                 stack = stackLayout,
-                onLockscreen = true
+                onLockscreen = true,
             )
         assertThat(space.whenEnoughSpace).isEqualTo(10f)
     }
@@ -272,7 +271,7 @@
                 visibleIndex = 0,
                 previousView = null,
                 stack = stackLayout,
-                onLockscreen = true
+                onLockscreen = true,
             )
         assertThat(space.whenEnoughSpace).isEqualTo(5)
     }
@@ -291,7 +290,7 @@
                 visibleIndex = 0,
                 previousView = null,
                 stack = stackLayout,
-                onLockscreen = true
+                onLockscreen = true,
             )
         assertThat(space.whenSavingSpace).isEqualTo(5)
     }
@@ -311,7 +310,7 @@
                 visibleIndex = 0,
                 previousView = null,
                 stack = stackLayout,
-                onLockscreen = true
+                onLockscreen = true,
             )
         assertThat(space.whenSavingSpace).isEqualTo(5)
     }
@@ -330,7 +329,7 @@
                 visibleIndex = 0,
                 previousView = null,
                 stack = stackLayout,
-                onLockscreen = false
+                onLockscreen = false,
             )
         assertThat(space.whenEnoughSpace).isEqualTo(rowHeight)
         assertThat(space.whenSavingSpace).isEqualTo(rowHeight)
@@ -340,14 +339,14 @@
         rows: List<ExpandableView>,
         spaceForNotifications: Float,
         spaceForShelf: Float,
-        shelfHeight: Float = this.shelfHeight
+        shelfHeight: Float = this.shelfHeight,
     ): Int {
         setupChildren(rows)
         return sizeCalculator.computeMaxKeyguardNotifications(
             stackLayout,
             spaceForNotifications,
             spaceForShelf,
-            shelfHeight
+            shelfHeight,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index a8bcfbc..39a1c10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.telephony.CellSignalStrength
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
@@ -735,9 +736,10 @@
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
     // See b/346904529 for more context
-    fun satBasedIcon_doesNotInflateSignalStrength() =
+    fun satBasedIcon_doesNotInflateSignalStrength_flagOff() =
         testScope.runTest {
             val latest by collectLastValue(underTest.signalLevelIcon)
 
@@ -756,7 +758,75 @@
             assertThat(latest!!.level).isEqualTo(4)
         }
 
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    // See b/346904529 for more context
+    fun satBasedIcon_doesNotInflateSignalStrength_flagOn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+            // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+            connectionRepository.inflateSignalStrength.value = true
+
+            connectionRepository.satelliteLevel.value = 4
+            assertThat(latest!!.level).isEqualTo(4)
+
+            connectionRepository.inflateSignalStrength.value = true
+            connectionRepository.primaryLevel.value = 4
+
+            // Icon level is unaffected
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    @Test
+    fun satBasedIcon_usesPrimaryLevel_flagOff() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+
+            // GIVEN primary level is set
+            connectionRepository.primaryLevel.value = 4
+            connectionRepository.satelliteLevel.value = 0
+
+            // THEN icon uses the primary level because the flag is off
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun satBasedIcon_usesSatelliteLevel_flagOn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+
+            // GIVEN satellite level is set
+            connectionRepository.satelliteLevel.value = 4
+            connectionRepository.primaryLevel.value = 0
+
+            // THEN icon uses the satellite level because the flag is on
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
+    /**
+     * Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is
+     * rolled out. The new API should report 0 automatically if not in service.
+     */
+    @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
     fun satBasedIcon_reportsLevelZeroWhenOutOfService() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 4c7cdfa..038722c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -151,7 +151,7 @@
                 iconsInteractor.isForceHidden,
                 repository,
                 context,
-                MobileIconCarrierIdOverridesFake()
+                MobileIconCarrierIdOverridesFake(),
             )
         createAndSetViewModel()
     }
@@ -359,7 +359,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             connectionsRepository.mobileIsDefault.value = true
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
@@ -406,7 +406,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
             repository.setDataEnabled(true)
@@ -426,7 +426,7 @@
             val initial =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
 
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
@@ -448,7 +448,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             repository.dataEnabled.value = true
             var latest: Icon? = null
@@ -477,7 +477,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -499,7 +499,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -520,7 +520,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -542,7 +542,7 @@
             val expected =
                 Icon.Resource(
                     connectionsRepository.defaultMobileIconGroup.value.dataType,
-                    ContentDescription.Resource(G.dataContentDescription)
+                    ContentDescription.Resource(G.dataContentDescription),
                 )
 
             assertThat(latest).isEqualTo(expected)
@@ -564,7 +564,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -621,10 +621,7 @@
                 underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -654,10 +651,7 @@
                 underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = false)
 
             yield()
 
@@ -666,20 +660,14 @@
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isTrue()
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -709,10 +697,7 @@
                 underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = false)
 
             yield()
 
@@ -721,20 +706,14 @@
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isTrue()
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -824,10 +803,7 @@
             // sets the background on cellular
             repository.hasPrioritizedNetworkCapabilities.value = true
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = true)
 
             assertThat(roaming).isFalse()
             assertThat(networkTypeIcon).isNull()
@@ -838,11 +814,13 @@
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
-    fun nonTerrestrial_usesSatelliteIcon() =
+    fun nonTerrestrial_usesSatelliteIcon_flagOff() =
         testScope.runTest {
             repository.isNonTerrestrial.value = true
             repository.setAllLevels(0)
+            repository.satelliteLevel.value = 0
 
             val latest by
                 collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
@@ -853,28 +831,66 @@
 
             // 1-2 -> 1 bar
             repository.setAllLevels(1)
+            repository.satelliteLevel.value = 1
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             repository.setAllLevels(2)
+            repository.satelliteLevel.value = 2
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             // 3-4 -> 2 bars
             repository.setAllLevels(3)
+            repository.satelliteLevel.value = 3
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
 
             repository.setAllLevels(4)
+            repository.satelliteLevel.value = 4
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun nonTerrestrial_usesSatelliteIcon_flagOn() =
+        testScope.runTest {
+            repository.isNonTerrestrial.value = true
+            repository.satelliteLevel.value = 0
+
+            val latest by
+                collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+            // Level 0 -> no connection
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+            // 1-2 -> 1 bar
+            repository.satelliteLevel.value = 1
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            repository.satelliteLevel.value = 2
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            // 3-4 -> 2 bars
+            repository.satelliteLevel.value = 3
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+            repository.satelliteLevel.value = 4
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
-    fun satelliteIcon_ignoresInflateSignalStrength() =
+    fun satelliteIcon_ignoresInflateSignalStrength_flagOff() =
         testScope.runTest {
             // Note that this is the exact same test as above, but with inflateSignalStrength set to
             // true we note that the level is unaffected by inflation
             repository.inflateSignalStrength.value = true
             repository.isNonTerrestrial.value = true
             repository.setAllLevels(0)
+            repository.satelliteLevel.value = 0
 
             val latest by
                 collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
@@ -885,16 +901,55 @@
 
             // 1-2 -> 1 bar
             repository.setAllLevels(1)
+            repository.satelliteLevel.value = 1
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             repository.setAllLevels(2)
+            repository.satelliteLevel.value = 2
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             // 3-4 -> 2 bars
             repository.setAllLevels(3)
+            repository.satelliteLevel.value = 3
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
 
             repository.setAllLevels(4)
+            repository.satelliteLevel.value = 4
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun satelliteIcon_ignoresInflateSignalStrength_flagOn() =
+        testScope.runTest {
+            // Note that this is the exact same test as above, but with inflateSignalStrength set to
+            // true we note that the level is unaffected by inflation
+            repository.inflateSignalStrength.value = true
+            repository.isNonTerrestrial.value = true
+            repository.satelliteLevel.value = 0
+
+            val latest by
+                collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+            // Level 0 -> no connection
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+            // 1-2 -> 1 bar
+            repository.satelliteLevel.value = 1
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            repository.satelliteLevel.value = 2
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            // 3-4 -> 2 bars
+            repository.satelliteLevel.value = 3
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+            repository.satelliteLevel.value = 4
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
index b5dbc3f..33223ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
@@ -71,6 +72,7 @@
     private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null)
 
     private val mainExecutor = FakeExecutor(FakeSystemClock())
+    private val userRepository = FakeUserRepository()
 
     private val testDispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(testDispatcher)
@@ -82,10 +84,13 @@
         // Never start in demo mode
         whenever(demoModeController.isInDemoMode).thenReturn(false)
 
-        whenever(wifiPickerTrackerFactory.create(any(), any(), any())).thenReturn(wifiPickerTracker)
+        whenever(wifiPickerTrackerFactory.create(any(), any(), any(), any()))
+            .thenReturn(wifiPickerTracker)
 
         realImpl =
             WifiRepositoryImpl(
+                mContext,
+                userRepository,
                 testScope.backgroundScope,
                 mainExecutor,
                 testDispatcher,
@@ -97,11 +102,7 @@
 
         whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow)
 
-        demoImpl =
-            DemoWifiRepository(
-                demoModeWifiDataSource,
-                testScope.backgroundScope,
-            )
+        demoImpl = DemoWifiRepository(demoModeWifiDataSource, testScope.backgroundScope)
 
         underTest =
             WifiRepositorySwitcher(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6b371d7..8a45930 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -19,6 +19,7 @@
 
 import android.app.admin.devicePolicyManager
 import android.content.pm.UserInfo
+import android.internal.statusbar.fakeStatusBarService
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
@@ -29,6 +30,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.res.R
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.model.SelectedUserModel
@@ -61,6 +63,7 @@
     private val globalSettings = kosmos.fakeGlobalSettings
     private val broadcastDispatcher = kosmos.broadcastDispatcher
     private val devicePolicyManager = kosmos.devicePolicyManager
+    private val statusBarService = kosmos.fakeStatusBarService
 
     @Mock private lateinit var manager: UserManager
 
@@ -72,6 +75,10 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         tracker = FakeUserTracker()
+        context.orCreateTestableResources.addOverride(
+            R.bool.config_userSwitchingMustGoThroughLoginScreen,
+            false,
+        )
     }
 
     @Test
@@ -323,6 +330,8 @@
             tracker = tracker,
             broadcastDispatcher = broadcastDispatcher,
             devicePolicyManager = devicePolicyManager,
+            resources = context.resources,
+            statusBarService = statusBarService,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt
index 26439df..f70b426 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt
@@ -49,35 +49,78 @@
     @Before
     fun setUp() {
         userRepository.setUserInfos(USER_INFOS)
-        runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[1]) }
+        runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[2]) }
+        userRepository.setLogoutToSystemUserEnabled(false)
+        userRepository.setSecondaryUserLogoutEnabled(false)
     }
 
     @Test
-    fun logOut_doesNothing_whenAdminDisabledSecondaryLogout() {
+    fun logOut_doesNothing_whenBothLogoutOptionsAreDisabled() {
         testScope.runTest {
             val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled)
-            val lastLogoutCount = userRepository.logOutSecondaryUserCallCount
-            userRepository.setSecondaryUserLogoutEnabled(false)
+            val secondaryUserLogoutCount = userRepository.logOutSecondaryUserCallCount
+            val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount
             assertThat(isLogoutEnabled).isFalse()
             underTest.logOut()
-            assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount)
+            assertThat(userRepository.logOutSecondaryUserCallCount)
+                .isEqualTo(secondaryUserLogoutCount)
+            assertThat(userRepository.logOutToSystemUserCallCount)
+                .isEqualTo(logoutToSystemUserCount)
         }
     }
 
     @Test
-    fun logOut_logsOut_whenAdminEnabledSecondaryLogout() {
+    fun logOut_logsOutSecondaryUser_whenAdminEnabledSecondaryLogout() {
         testScope.runTest {
             val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled)
             val lastLogoutCount = userRepository.logOutSecondaryUserCallCount
+            val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount
             userRepository.setSecondaryUserLogoutEnabled(true)
             assertThat(isLogoutEnabled).isTrue()
             underTest.logOut()
             assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1)
+            assertThat(userRepository.logOutToSystemUserCallCount)
+                .isEqualTo(logoutToSystemUserCount)
+        }
+    }
+
+    @Test
+    fun logOut_logsOutToSystemUser_whenLogoutToSystemUserIsEnabled() {
+        testScope.runTest {
+            val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled)
+            val lastLogoutCount = userRepository.logOutSecondaryUserCallCount
+            val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount
+            userRepository.setLogoutToSystemUserEnabled(true)
+            assertThat(isLogoutEnabled).isTrue()
+            underTest.logOut()
+            assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount)
+            assertThat(userRepository.logOutToSystemUserCallCount)
+                .isEqualTo(logoutToSystemUserCount + 1)
+        }
+    }
+
+    @Test
+    fun logOut_secondaryUserTakesPrecedence() {
+        testScope.runTest {
+            val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled)
+            val lastLogoutCount = userRepository.logOutSecondaryUserCallCount
+            val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount
+            userRepository.setLogoutToSystemUserEnabled(true)
+            userRepository.setSecondaryUserLogoutEnabled(true)
+            assertThat(isLogoutEnabled).isTrue()
+            underTest.logOut()
+            assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1)
+            assertThat(userRepository.logOutToSystemUserCallCount)
+                .isEqualTo(logoutToSystemUserCount)
         }
     }
 
     companion object {
         private val USER_INFOS =
-            listOf(UserInfo(0, "System user", 0), UserInfo(10, "Regular user", 0))
+            listOf(
+                UserInfo(0, "System user", 0),
+                UserInfo(10, "Regular user", 0),
+                UserInfo(11, "Secondary user", 0),
+            )
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt
index e88dbd2..ad473c0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt
@@ -16,152 +16,16 @@
 
 package com.android.systemui.util.settings.repository
 
-import android.content.pm.UserInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.coroutines.collectValues
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.fakeUserRepository
 import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
-import com.android.systemui.util.settings.fakeSettings
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class UserAwareSecureSettingsRepositoryTest : SysuiTestCase() {
+class UserAwareSecureSettingsRepositoryTest : UserAwareSettingsRepositoryTestBase() {
 
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-    private val secureSettings = kosmos.fakeSettings
-    private val userRepository = kosmos.fakeUserRepository
-    private lateinit var underTest: UserAwareSecureSettingsRepository
-
-    @Before
-    fun setup() {
-        underTest = kosmos.userAwareSecureSettingsRepository
-
-        userRepository.setUserInfos(USER_INFOS)
-
-        secureSettings.putBoolForUser(BOOL_SETTING_NAME, true, USER_1.id)
-        secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_2.id)
-        secureSettings.putIntForUser(INT_SETTING_NAME, 1337, USER_1.id)
-        secureSettings.putIntForUser(INT_SETTING_NAME, 818, USER_2.id)
-    }
-
-    @Test
-    fun boolSetting_emitsInitialValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-
-            val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false))
-
-            assertThat(enabled).isTrue()
-        }
-    }
-
-    @Test
-    fun boolSetting_whenSettingChanges_emitsNewValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-            val enabled by collectValues(underTest.boolSetting(BOOL_SETTING_NAME, false))
-            runCurrent()
-
-            secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_1.id)
-
-            assertThat(enabled).containsExactly(true, false).inOrder()
-        }
-    }
-
-    @Test
-    fun boolSetting_whenWhenUserChanges_emitsNewValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-            val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false))
-            runCurrent()
-
-            userRepository.setSelectedUserInfo(USER_2)
-
-            assertThat(enabled).isFalse()
-        }
-    }
-
-    @Test
-    fun intSetting_emitsInitialValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-
-            val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0))
-
-            assertThat(number).isEqualTo(1337)
-        }
-    }
-
-    @Test
-    fun intSetting_whenSettingChanges_emitsNewValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-            val number by collectValues(underTest.intSetting(INT_SETTING_NAME, 0))
-            runCurrent()
-
-            secureSettings.putIntForUser(INT_SETTING_NAME, 1338, USER_1.id)
-
-            assertThat(number).containsExactly(1337, 1338).inOrder()
-        }
-    }
-
-    @Test
-    fun intSetting_whenWhenUserChanges_emitsNewValue() {
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-            val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0))
-            runCurrent()
-
-            userRepository.setSelectedUserInfo(USER_2)
-
-            assertThat(number).isEqualTo(818)
-        }
-    }
-
-    @Test
-    fun getInt_returnsInitialValue() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-
-            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(1337)
-        }
-
-    @Test
-    fun getInt_whenSettingChanges_returnsNewValue() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_1)
-            secureSettings.putIntForUser(INT_SETTING_NAME, 999, USER_1.id)
-
-            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(999)
-        }
-
-    @Test
-    fun getInt_whenUserChanges_returnsThatUserValue() =
-        testScope.runTest {
-            userRepository.setSelectedUserInfo(USER_2)
-
-            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(818)
-        }
-
-    private companion object {
-        const val BOOL_SETTING_NAME = "BOOL_SETTING_NAME"
-        const val INT_SETTING_NAME = "INT_SETTING_NAME"
-        val USER_1 = UserInfo(/* id= */ 0, "user1", /* flags= */ 0)
-        val USER_2 = UserInfo(/* id= */ 1, "user2", /* flags= */ 0)
-        val USER_INFOS = listOf(USER_1, USER_2)
+    override fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository {
+        return kosmos.userAwareSecureSettingsRepository
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt
new file mode 100644
index 0000000..09db96f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.util.settings.repository
+
+import android.content.pm.UserInfo
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+abstract class UserAwareSettingsRepositoryTestBase : SysuiTestCase() {
+
+    protected val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    protected val secureSettings = kosmos.fakeSettings
+    protected val userRepository = kosmos.fakeUserRepository
+    private lateinit var underTest: UserAwareSettingsRepository
+
+    @Before
+    fun setup() {
+        underTest = getKosmosUserAwareSettingsRepository()
+
+        userRepository.setUserInfos(USER_INFOS)
+
+        secureSettings.putBoolForUser(BOOL_SETTING_NAME, true, USER_1.id)
+        secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_2.id)
+        secureSettings.putIntForUser(INT_SETTING_NAME, 1337, USER_1.id)
+        secureSettings.putIntForUser(INT_SETTING_NAME, 818, USER_2.id)
+    }
+
+    abstract fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository
+
+    @Test
+    fun boolSetting_emitsInitialValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+
+            val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false))
+
+            assertThat(enabled).isTrue()
+        }
+    }
+
+    @Test
+    fun boolSetting_whenSettingChanges_emitsNewValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+            val enabled by collectValues(underTest.boolSetting(BOOL_SETTING_NAME, false))
+            runCurrent()
+
+            secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_1.id)
+
+            assertThat(enabled).containsExactly(true, false).inOrder()
+        }
+    }
+
+    @Test
+    fun boolSetting_whenWhenUserChanges_emitsNewValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+            val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false))
+            runCurrent()
+
+            userRepository.setSelectedUserInfo(USER_2)
+
+            assertThat(enabled).isFalse()
+        }
+    }
+
+    @Test
+    fun intSetting_emitsInitialValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+
+            val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0))
+
+            assertThat(number).isEqualTo(1337)
+        }
+    }
+
+    @Test
+    fun intSetting_whenSettingChanges_emitsNewValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+            val number by collectValues(underTest.intSetting(INT_SETTING_NAME, 0))
+            runCurrent()
+
+            secureSettings.putIntForUser(INT_SETTING_NAME, 1338, USER_1.id)
+
+            assertThat(number).containsExactly(1337, 1338).inOrder()
+        }
+    }
+
+    @Test
+    fun intSetting_whenWhenUserChanges_emitsNewValue() {
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+            val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0))
+            runCurrent()
+
+            userRepository.setSelectedUserInfo(USER_2)
+
+            assertThat(number).isEqualTo(818)
+        }
+    }
+
+    @Test
+    fun getInt_returnsInitialValue() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+
+            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(1337)
+        }
+
+    @Test
+    fun getInt_whenSettingChanges_returnsNewValue() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_1)
+            secureSettings.putIntForUser(INT_SETTING_NAME, 999, USER_1.id)
+
+            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(999)
+        }
+
+    @Test
+    fun getInt_whenUserChanges_returnsThatUserValue() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(USER_2)
+
+            assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(818)
+        }
+
+    private companion object {
+        const val BOOL_SETTING_NAME = "BOOL_SETTING_NAME"
+        const val INT_SETTING_NAME = "INT_SETTING_NAME"
+        val USER_1 = UserInfo(/* id= */ 0, "user1", /* flags= */ 0)
+        val USER_2 = UserInfo(/* id= */ 1, "user2", /* flags= */ 0)
+        val USER_INFOS = listOf(USER_1, USER_2)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.kt
new file mode 100644
index 0000000..586da8e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.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.util.settings.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.util.settings.data.repository.userAwareSystemSettingsRepository
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UserAwareSystemSettingsRepositoryTest : UserAwareSettingsRepositoryTestBase() {
+
+    override fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository {
+        return kosmos.userAwareSystemSettingsRepository
+    }
+}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 82c8c44..0854eb4 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1086,4 +1086,9 @@
     enable the desktop specific features.
     -->
     <bool name="config_enableDesktopFeatureSet">false</bool>
+
+    <!--
+    Whether the user switching can only happen by logging out and going through the system user (login screen).
+    -->
+    <bool name="config_userSwitchingMustGoThroughLoginScreen">false</bool>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 24d61911..df9f705 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -35,6 +35,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.customization.R
 import com.android.systemui.dagger.qualifiers.Background
@@ -62,6 +63,7 @@
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.plugins.clocks.ZenData.ZenMode
 import com.android.systemui.res.R as SysuiR
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.regionsampling.RegionSampler
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -80,7 +82,6 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
@@ -103,6 +104,7 @@
     private val featureFlags: FeatureFlagsClassic,
     private val zenModeController: ZenModeController,
     private val zenModeInteractor: ZenModeInteractor,
+    private val userTracker: UserTracker,
 ) {
     var loggers =
         listOf(
@@ -120,6 +122,10 @@
             connectClock(value)
         }
 
+    private fun is24HourFormat(userId: Int? = null): Boolean {
+        return DateFormat.is24HourFormat(context, userId ?: userTracker.userId)
+    }
+
     private fun disconnectClock(clock: ClockController?) {
         if (clock == null) {
             return
@@ -186,7 +192,7 @@
                 var pastVisibility: Int? = null
 
                 override fun onViewAttachedToWindow(view: View) {
-                    clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                    clock.events.onTimeFormatChanged(is24HourFormat())
                     // Match the asing for view.parent's layout classes.
                     smallClockFrame =
                         (view.parent as ViewGroup)?.also { frame ->
@@ -218,7 +224,7 @@
         largeClockOnAttachStateChangeListener =
             object : OnAttachStateChangeListener {
                 override fun onViewAttachedToWindow(p0: View) {
-                    clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                    clock.events.onTimeFormatChanged(is24HourFormat())
                 }
 
                 override fun onViewDetachedFromWindow(p0: View) {}
@@ -358,7 +364,7 @@
             }
 
             override fun onTimeFormatChanged(timeFormat: String?) {
-                clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
+                clock?.run { events.onTimeFormatChanged(is24HourFormat()) }
             }
 
             override fun onTimeZoneChanged(timeZone: TimeZone) {
@@ -366,7 +372,7 @@
             }
 
             override fun onUserSwitchComplete(userId: Int) {
-                clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
+                clock?.run { events.onTimeFormatChanged(is24HourFormat(userId)) }
                 zenModeCallback.onNextAlarmChanged()
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 811b47d..a46b236 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui;
 
+import android.animation.Animator;
 import android.annotation.SuppressLint;
 import android.app.ActivityThread;
 import android.app.Application;
@@ -135,6 +136,9 @@
         if (Flags.enableLayoutTracing()) {
             View.setTraceLayoutSteps(true);
         }
+        if (com.android.window.flags.Flags.systemUiPostAnimationEnd()) {
+            Animator.setPostNotifyEndListenerEnabled(true);
+        }
 
         if (mProcessWrapper.isSystemUser()) {
             IntentFilter bootCompletedFilter = new
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index c9c4fd5..6635d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -22,6 +22,7 @@
 import android.app.admin.DevicePolicyManager
 import android.content.IntentFilter
 import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel
@@ -57,7 +58,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.withContext
 
 /** Defines interface for classes that can access authentication-related application state. */
@@ -178,6 +178,16 @@
      * profile of an organization-owned device.
      */
     @UserIdInt suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int
+
+    /**
+     * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the
+     * device goes to sleep, this is the maximum time the device policy allows to wait before
+     * locking the device, despite what the user setting might be set to.
+     */
+    suspend fun getMaximumTimeToLock(): Long
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    suspend fun getPowerButtonInstantlyLocks(): Boolean
 }
 
 @SysUISingleton
@@ -324,6 +334,19 @@
         }
     }
 
+    override suspend fun getMaximumTimeToLock(): Long {
+        return withContext(backgroundDispatcher) {
+            devicePolicyManager.getMaximumTimeToLock(/* admin= */ null, selectedUserId)
+        }
+    }
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    override suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return withContext(backgroundDispatcher) {
+            lockPatternUtils.getPowerButtonInstantlyLocks(selectedUserId)
+        }
+    }
+
     private val selectedUserId: Int
         @UserIdInt get() = userRepository.getSelectedUserInfo().id
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 67579fd..1c99473 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.authentication.domain.interactor
 
 import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
@@ -49,7 +50,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Hosts application business logic related to user authentication.
@@ -215,7 +215,7 @@
      */
     suspend fun authenticate(
         input: List<Any>,
-        tryAutoConfirm: Boolean = false
+        tryAutoConfirm: Boolean = false,
     ): AuthenticationResult {
         if (input.isEmpty()) {
             throw IllegalArgumentException("Input was empty!")
@@ -254,6 +254,20 @@
         return AuthenticationResult.FAILED
     }
 
+    /**
+     * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the
+     * device goes to sleep, this is the maximum time the device policy allows to wait before
+     * locking the device, despite what the user setting might be set to.
+     */
+    suspend fun getMaximumTimeToLock(): Long {
+        return repository.getMaximumTimeToLock()
+    }
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return !getAuthenticationMethod().isSecure || repository.getPowerButtonInstantlyLocks()
+    }
+
     private suspend fun shouldSkipAuthenticationAttempt(
         authenticationMethod: AuthenticationMethodModel,
         isAutoConfirmAttempt: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
index f825459..a107322 100644
--- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
 import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
 import com.android.systemui.people.widget.PeopleBackupHelper
+import com.android.systemui.qs.panels.domain.backup.QSPreferencesBackupHelper
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserFileManagerImpl
 
@@ -58,9 +59,9 @@
         private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences"
         private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY =
             "systemui.keyguard.quickaffordance.shared_preferences"
-        private const val COMMUNAL_PREFS_BACKUP_KEY =
-            "systemui.communal.shared_preferences"
+        private const val COMMUNAL_PREFS_BACKUP_KEY = "systemui.communal.shared_preferences"
         private const val COMMUNAL_STATE_BACKUP_KEY = "systemui.communal_state"
+        private const val QS_PREFERENCES_BACKUP_KEY = "systemui.qs.shared_preferences"
         val controlsDataLock = Any()
         const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
         const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
@@ -74,22 +75,20 @@
         val keys = PeopleBackupHelper.getFilesToBackup()
         addHelper(
             PEOPLE_TILES_BACKUP_KEY,
-            PeopleBackupHelper(this, userHandle, keys.toTypedArray())
+            PeopleBackupHelper(this, userHandle, keys.toTypedArray()),
         )
         addHelper(
             KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY,
-            KeyguardQuickAffordanceBackupHelper(
-                context = this,
-                userId = userHandle.identifier,
-            ),
+            KeyguardQuickAffordanceBackupHelper(context = this, userId = userHandle.identifier),
+        )
+        addHelper(
+            QS_PREFERENCES_BACKUP_KEY,
+            QSPreferencesBackupHelper(context = this, userId = userHandle.identifier),
         )
         if (communalEnabled()) {
             addHelper(
                 COMMUNAL_PREFS_BACKUP_KEY,
-                CommunalPrefsBackupHelper(
-                    context = this,
-                    userId = userHandle.identifier,
-                )
+                CommunalPrefsBackupHelper(context = this, userId = userHandle.identifier),
             )
             addHelper(
                 COMMUNAL_STATE_BACKUP_KEY,
@@ -110,10 +109,7 @@
     }
 
     private fun addControlsHelper(userId: Int) {
-        val file = UserFileManagerImpl.createFile(
-            userId = userId,
-            fileName = CONTROLS,
-        )
+        val file = UserFileManagerImpl.createFile(userId = userId, fileName = CONTROLS)
         // The map in mapOf is guaranteed to be order preserving
         val controlsMap = mapOf(file.getPath() to getPPControlsFile(this, userId))
         NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also {
@@ -134,6 +130,7 @@
      * @property lock a lock to hold while backing up and restoring the files.
      * @property context the context of the [BackupAgent]
      * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing
+     *
      * ```
      *                                   actions to take
      * ```
@@ -141,7 +138,7 @@
     private class NoOverwriteFileBackupHelper(
         val lock: Any,
         val context: Context,
-        val fileNamesAndPostProcess: Map<String, () -> Unit>
+        val fileNamesAndPostProcess: Map<String, () -> Unit>,
     ) : FileBackupHelper(context, *fileNamesAndPostProcess.keys.toTypedArray()) {
 
         override fun restoreEntity(data: BackupDataInputStream) {
@@ -152,11 +149,12 @@
                 return
             }
             synchronized(lock) {
-                traceSection("File restore: ${data.key}") {
-                    super.restoreEntity(data)
-                }
-                Log.d(TAG, "Finishing restore for ${data.key} for user ${context.userId}. " +
-                        "Starting postProcess.")
+                traceSection("File restore: ${data.key}") { super.restoreEntity(data) }
+                Log.d(
+                    TAG,
+                    "Finishing restore for ${data.key} for user ${context.userId}. " +
+                        "Starting postProcess.",
+                )
                 traceSection("Postprocess: ${data.key}") {
                     fileNamesAndPostProcess.get(data.key)?.invoke()
                 }
@@ -167,7 +165,7 @@
         override fun performBackup(
             oldState: ParcelFileDescriptor?,
             data: BackupDataOutput?,
-            newState: ParcelFileDescriptor?
+            newState: ParcelFileDescriptor?,
         ) {
             synchronized(lock) { super.performBackup(oldState, data, newState) }
         }
@@ -176,15 +174,13 @@
 
 private fun getPPControlsFile(context: Context, userId: Int): () -> Unit {
     return {
-        val file = UserFileManagerImpl.createFile(
-            userId = userId,
-            fileName = BackupHelper.CONTROLS,
-        )
+        val file = UserFileManagerImpl.createFile(userId = userId, fileName = BackupHelper.CONTROLS)
         if (file.exists()) {
-            val dest = UserFileManagerImpl.createFile(
-                userId = userId,
-                fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME,
-            )
+            val dest =
+                UserFileManagerImpl.createFile(
+                    userId = userId,
+                    fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME,
+                )
             file.copyTo(dest)
             val jobScheduler = context.getSystemService(JobScheduler::class.java)
             jobScheduler?.schedule(
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
index c464a66..6c335e7 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt
@@ -18,13 +18,18 @@
 
 import com.android.keyguard.EmptyLockIconViewController
 import com.android.keyguard.LockIconViewController
+import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule
 import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import dagger.Binds
 import dagger.Lazy
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import dagger.multibindings.Multibinds
 
 @Module(includes = [DeviceEntryRepositoryModule::class, FaceWakeUpTriggersConfigModule::class])
@@ -34,6 +39,13 @@
      */
     @Multibinds abstract fun deviceEntryIconTransitionSet(): Set<DeviceEntryIconTransition>
 
+    @Binds
+    @IntoMap
+    @ClassKey(DeviceUnlockedInteractor.Activator::class)
+    abstract fun deviceUnlockedInteractorActivator(
+        activator: DeviceUnlockedInteractor.Activator
+    ): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
index 3f937bb..675f00a 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
@@ -5,6 +5,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.user.data.repository.UserRepository
 import dagger.Binds
@@ -42,6 +43,8 @@
      */
     val isBypassEnabled: StateFlow<Boolean>
 
+    val deviceUnlockStatus: MutableStateFlow<DeviceUnlockStatus>
+
     /**
      * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has
      * chosen any secure authentication method and even if they set the lockscreen to be dismissed
@@ -84,6 +87,9 @@
                 initialValue = keyguardBypassController.bypassEnabled,
             )
 
+    override val deviceUnlockStatus =
+        MutableStateFlow(DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null))
+
     override suspend fun isLockscreenEnabled(): Boolean {
         return withContext(backgroundDispatcher) {
             val selectedUserId = userRepository.getSelectedUserInfo().id
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index 5259c5d..b74ca03 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -16,7 +16,10 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import android.provider.Settings
+import android.util.Log
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.CoreStartable
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.dagger.SysUISingleton
@@ -26,42 +29,51 @@
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.TrustInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
 import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class DeviceUnlockedInteractor
 @Inject
 constructor(
-    @Application private val applicationScope: CoroutineScope,
-    authenticationInteractor: AuthenticationInteractor,
-    deviceEntryRepository: DeviceEntryRepository,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val repository: DeviceEntryRepository,
     trustInteractor: TrustInteractor,
     faceAuthInteractor: DeviceEntryFaceAuthInteractor,
     fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
     private val powerInteractor: PowerInteractor,
     private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
     private val systemPropertiesHelper: SystemPropertiesHelper,
-    keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) {
+    private val userAwareSecureSettingsRepository: UserAwareSecureSettingsRepository,
+    private val keyguardInteractor: KeyguardInteractor,
+) : ExclusiveActivatable() {
 
     private val deviceUnlockSource =
         merge(
@@ -69,7 +81,7 @@
             faceAuthInteractor.isAuthenticated
                 .filter { it }
                 .map {
-                    if (deviceEntryRepository.isBypassEnabled.value) {
+                    if (repository.isBypassEnabled.value) {
                         DeviceUnlockSource.FaceWithBypass
                     } else {
                         DeviceUnlockSource.FaceWithoutBypass
@@ -163,43 +175,160 @@
      * proceed.
      */
     val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> =
-        authenticationInteractor.authenticationMethod
-            .flatMapLatest { authMethod ->
-                if (!authMethod.isSecure) {
-                    flowOf(DeviceUnlockStatus(true, null))
-                } else if (authMethod == AuthenticationMethodModel.Sim) {
-                    // Device is locked if SIM is locked.
-                    flowOf(DeviceUnlockStatus(false, null))
-                } else {
-                    combine(
-                            powerInteractor.isAsleep,
-                            isInLockdown,
-                            keyguardTransitionInteractor
-                                .transitionValue(KeyguardState.AOD)
-                                .map { it == 1f }
-                                .distinctUntilChanged(),
-                            ::Triple,
-                        )
-                        .flatMapLatestConflated { (isAsleep, isInLockdown, isAod) ->
-                            val isForceLocked =
-                                when {
-                                    isAsleep && !isAod -> true
-                                    isInLockdown -> true
-                                    else -> false
-                                }
-                            if (isForceLocked) {
-                                flowOf(DeviceUnlockStatus(false, null))
+        repository.deviceUnlockStatus.asStateFlow()
+
+    override suspend fun onActivated(): Nothing {
+        authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
+            if (!authMethod.isSecure) {
+                // Device remains unlocked as long as the authentication method is not secure.
+                Log.d(TAG, "remaining unlocked because auth method not secure")
+                repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, null)
+            } else if (authMethod == AuthenticationMethodModel.Sim) {
+                // Device remains locked while SIM is locked.
+                Log.d(TAG, "remaining locked because SIM locked")
+                repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
+            } else {
+                handleLockAndUnlockEvents()
+            }
+        }
+
+        awaitCancellation()
+    }
+
+    private suspend fun handleLockAndUnlockEvents() {
+        try {
+            Log.d(TAG, "started watching for lock and unlock events")
+            coroutineScope {
+                launch { handleUnlockEvents() }
+                launch { handleLockEvents() }
+            }
+        } finally {
+            Log.d(TAG, "stopped watching for lock and unlock events")
+        }
+    }
+
+    private suspend fun handleUnlockEvents() {
+        // Unlock the device when a new unlock source is detected.
+        deviceUnlockSource.collect {
+            Log.d(TAG, "unlocking due to \"$it\"")
+            repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it)
+        }
+    }
+
+    private suspend fun handleLockEvents() {
+        merge(
+                // Device wakefulness events.
+                powerInteractor.detailedWakefulness
+                    .map { Pair(it.isAsleep(), it.lastSleepReason) }
+                    .distinctUntilChangedBy { it.first }
+                    .map { (isAsleep, lastSleepReason) ->
+                        if (isAsleep) {
+                            if (
+                                lastSleepReason == WakeSleepReason.POWER_BUTTON &&
+                                    authenticationInteractor.getPowerButtonInstantlyLocks()
+                            ) {
+                                LockImmediately("locked instantly from power button")
                             } else {
-                                deviceUnlockSource.map { DeviceUnlockStatus(true, it) }
+                                LockWithDelay("entering sleep")
+                            }
+                        } else {
+                            CancelDelayedLock("waking up")
+                        }
+                    },
+                // Device enters lockdown.
+                isInLockdown
+                    .distinctUntilChanged()
+                    .filter { it }
+                    .map { LockImmediately("lockdown") },
+                // Started dreaming
+                powerInteractor.isInteractive.flatMapLatestConflated { isInteractive ->
+                    // Only respond to dream state changes while the device is interactive.
+                    if (isInteractive) {
+                        keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming ->
+                            if (isDreaming) {
+                                LockWithDelay("started dreaming")
+                            } else {
+                                CancelDelayedLock("stopped dreaming")
                             }
                         }
+                    } else {
+                        emptyFlow()
+                    }
+                },
+            )
+            .collectLatest(::onLockEvent)
+    }
+
+    private suspend fun onLockEvent(event: LockEvent) {
+        val debugReason = event.debugReason
+        when (event) {
+            is LockImmediately -> {
+                Log.d(TAG, "locking without delay due to \"$debugReason\"")
+                repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
+            }
+
+            is LockWithDelay -> {
+                val lockDelay = lockDelay()
+                Log.d(TAG, "locking in ${lockDelay}ms due to \"$debugReason\"")
+                try {
+                    delay(lockDelay)
+                    Log.d(
+                        TAG,
+                        "locking after having waited for ${lockDelay}ms due to \"$debugReason\"",
+                    )
+                    repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
+                } catch (_: CancellationException) {
+                    Log.d(
+                        TAG,
+                        "delayed locking canceled, original delay was ${lockDelay}ms and reason was \"$debugReason\"",
+                    )
                 }
             }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.Eagerly,
-                initialValue = DeviceUnlockStatus(false, null),
-            )
+
+            is CancelDelayedLock -> {
+                // Do nothing, the mere receipt of this inside of a "latest" block means that any
+                // previous coroutine is automatically canceled.
+            }
+        }
+    }
+
+    /**
+     * Returns the amount of time to wait before locking down the device after the device has been
+     * put to sleep by the user, in milliseconds.
+     */
+    private suspend fun lockDelay(): Long {
+        val lockAfterScreenTimeoutSetting =
+            userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        Log.d(TAG, "Lock after screen timeout setting set to ${lockAfterScreenTimeoutSetting}ms")
+
+        val maxTimeToLockDevicePolicy = authenticationInteractor.getMaximumTimeToLock()
+        Log.d(TAG, "Device policy max set to ${maxTimeToLockDevicePolicy}ms")
+
+        if (maxTimeToLockDevicePolicy <= 0) {
+            // No device policy enforced maximum.
+            Log.d(TAG, "No device policy max, delay is ${lockAfterScreenTimeoutSetting}ms")
+            return lockAfterScreenTimeoutSetting
+        }
+
+        val screenOffTimeoutSetting =
+            userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.System.SCREEN_OFF_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT,
+                )
+                .coerceAtLeast(0)
+                .toLong()
+        Log.d(TAG, "Screen off timeout setting set to ${screenOffTimeoutSetting}ms")
+
+        return (maxTimeToLockDevicePolicy - screenOffTimeoutSetting)
+            .coerceIn(minimumValue = 0, maximumValue = lockAfterScreenTimeoutSetting)
+            .also { Log.d(TAG, "Device policy max enforced, delay is ${it}ms") }
+    }
 
     private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean {
         return when (this) {
@@ -226,7 +355,30 @@
         return systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE
     }
 
+    /** [CoreStartable] that activates the [DeviceUnlockedInteractor]. */
+    class Activator
+    @Inject
+    constructor(
+        @Application private val applicationScope: CoroutineScope,
+        private val interactor: DeviceUnlockedInteractor,
+    ) : CoreStartable {
+        override fun start() {
+            applicationScope.launch { interactor.activate() }
+        }
+    }
+
+    private sealed interface LockEvent {
+        val debugReason: String
+    }
+
+    private data class LockImmediately(override val debugReason: String) : LockEvent
+
+    private data class LockWithDelay(override val debugReason: String) : LockEvent
+
+    private data class CancelDelayedLock(override val debugReason: String) : LockEvent
+
     companion object {
+        private val TAG = "DeviceUnlockedInteractor"
         @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
         @VisibleForTesting const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index dd08d32..7a95a41 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -40,7 +40,6 @@
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Flags;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
@@ -566,8 +565,7 @@
         }
 
         // When already in pulsing, we can show the new Notification without requesting a new pulse.
-        if (Flags.notificationPulsingFix()
-                && dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) {
+        if (dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) {
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
index 585f7ed..922bc15 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
@@ -52,6 +52,7 @@
 constructor(
     private val inputManager: InputManager,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    // TODO: b/377244768 - Change to inject SecureSettingsRepository
     secureSettingsRepository: UserAwareSecureSettingsRepository,
     private val stickyKeysLogger: StickyKeysLogger,
 ) : StickyKeysRepository {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 60a306b..2ee9ddb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -239,7 +239,7 @@
 
     private static final boolean ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS =
             Flags.ensureKeyguardDoesTransitionStarting();
-    private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
+    public static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
     private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000;
 
     private static final boolean DEBUG = KeyguardConstants.DEBUG;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 3a5614f..eaf8fa9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -114,6 +114,18 @@
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState,
     )
+
+    /**
+     * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that
+     * otherwise would have been emitted.
+     *
+     * When the screen is off, upcoming performance changes cause all Animators to cease emitting
+     * frames, which means the Animator passed to [startTransition] will never finish if it was
+     * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when
+     * the screen isn't even on. As long as we emit FINISHED, everything should end up in the
+     * correct state.
+     */
+    suspend fun forceFinishCurrentTransition()
 }
 
 @SysUISingleton
@@ -134,6 +146,7 @@
     override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
     private var lastStep: TransitionStep = TransitionStep()
     private var lastAnimator: ValueAnimator? = null
+    private var animatorListener: AnimatorListenerAdapter? = null
 
     private val withContextMutex = Mutex()
     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
@@ -233,7 +246,7 @@
                     )
                 }
 
-                val adapter =
+                animatorListener =
                     object : AnimatorListenerAdapter() {
                         override fun onAnimationStart(animation: Animator) {
                             emitTransition(
@@ -254,9 +267,10 @@
                             animator.removeListener(this)
                             animator.removeUpdateListener(updateListener)
                             lastAnimator = null
+                            animatorListener = null
                         }
                     }
-                animator.addListener(adapter)
+                animator.addListener(animatorListener)
                 animator.addUpdateListener(updateListener)
                 animator.start()
                 return@withContext null
@@ -290,6 +304,33 @@
         }
     }
 
+    override suspend fun forceFinishCurrentTransition() {
+        withContextMutex.lock()
+
+        if (lastAnimator?.isRunning != true) {
+            return
+        }
+
+        return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) {
+            withContextMutex.unlock()
+
+            Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.")
+
+            lastAnimator?.apply {
+                // Cancel the animator, but remove listeners first so we don't emit CANCELED.
+                removeAllListeners()
+                cancel()
+
+                // Emit a final 1f RUNNING step to ensure that any transitions not listening for a
+                // FINISHED step end up in the right end state.
+                emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING))
+
+                // Ask the listener to emit FINISHED and clean up its state.
+                animatorListener?.onAnimationEnd(this)
+            }
+        }
+    }
+
     private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index b815f19..7cd2744 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,8 +19,10 @@
 
 import android.annotation.SuppressLint
 import android.util.Log
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
@@ -30,6 +32,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
@@ -59,7 +63,6 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Encapsulates business-logic related to the keyguard transitions. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -70,6 +73,7 @@
     @Application val scope: CoroutineScope,
     private val repository: KeyguardTransitionRepository,
     private val sceneInteractor: SceneInteractor,
+    private val powerInteractor: PowerInteractor,
 ) {
     private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()
 
@@ -188,6 +192,18 @@
                     }
                 }
         }
+
+        if (keyguardTransitionForceFinishOnScreenOff()) {
+            /**
+             * If the screen is turning off, finish the current transition immediately. Further
+             * frames won't be visible anyway.
+             */
+            scope.launch {
+                powerInteractor.screenPowerState
+                    .filter { it == ScreenPowerState.SCREEN_TURNING_OFF }
+                    .collect { repository.forceFinishCurrentTransition() }
+            }
+        }
     }
 
     fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index c11005d..a595d81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -93,18 +93,18 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {}
 
         override fun createAnimator(
             sceenRoot: ViewGroup,
             startValues: TransitionValues?,
-            endValues: TransitionValues?
+            endValues: TransitionValues?,
         ): Animator? {
             if (startValues == null || endValues == null) {
                 Log.w(
                     TAG,
-                    "Couldn't create animator: startValues=$startValues; endValues=$endValues"
+                    "Couldn't create animator: startValues=$startValues; endValues=$endValues",
                 )
                 return null
             }
@@ -137,7 +137,7 @@
                         "Skipping no-op transition: $toView; " +
                             "vis: $fromVis -> $toVis; " +
                             "alpha: $fromAlpha -> $toAlpha; " +
-                            "bounds: $fromBounds -> $toBounds; "
+                            "bounds: $fromBounds -> $toBounds; ",
                     )
                 }
                 return null
@@ -151,7 +151,7 @@
                     lerp(fromBounds.left, toBounds.left, fract),
                     lerp(fromBounds.top, toBounds.top, fract),
                     lerp(fromBounds.right, toBounds.right, fract),
-                    lerp(fromBounds.bottom, toBounds.bottom, fract)
+                    lerp(fromBounds.bottom, toBounds.bottom, fract),
                 )
 
             fun assignAnimValues(src: String, fract: Float, vis: Int? = null) {
@@ -160,7 +160,7 @@
                 if (DEBUG) {
                     Log.i(
                         TAG,
-                        "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;"
+                        "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;",
                     )
                 }
                 toView.setVisibility(vis ?: View.VISIBLE)
@@ -174,7 +174,7 @@
                     "transitioning: $toView; " +
                         "vis: $fromVis -> $toVis; " +
                         "alpha: $fromAlpha -> $toAlpha; " +
-                        "bounds: $fromBounds -> $toBounds; "
+                        "bounds: $fromBounds -> $toBounds; ",
                 )
             }
 
@@ -258,7 +258,7 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {
             // Move normally if clock is not changing visibility
             if (fromIsVis == toIsVis) return
@@ -347,12 +347,17 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {
             // If view is changing visibility, hold it in place
             if (fromIsVis == toIsVis) return
             if (DEBUG) Log.i(TAG, "Holding position of ${view.id}")
-            toBounds.set(fromBounds)
+
+            if (fromIsVis) {
+                toBounds.set(fromBounds)
+            } else {
+                fromBounds.set(toBounds)
+            }
         }
 
         companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
index d38e507..913aa6f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
@@ -42,7 +42,7 @@
 import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.controls.util.SessionTokenFactory
 import com.android.systemui.res.R
-import com.android.systemui.util.Assert
+import com.android.systemui.util.concurrency.Execution
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -63,6 +63,7 @@
     @Background private val looper: Looper,
     @Background private val handler: Handler,
     @Background private val bgScope: CoroutineScope,
+    private val execution: Execution,
 ) {
 
     /**
@@ -108,7 +109,7 @@
         m3controller: Media3Controller,
         token: SessionToken,
     ): MediaButton? {
-        Assert.isNotMainThread()
+        require(!execution.isMainThread())
 
         // First, get standard actions
         val playOrPause =
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index f49693a..80ac2fc 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -696,7 +696,8 @@
                 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                         mTaskStackListener);
                 DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
-                mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null));
+                mPipOptional.ifPresent(pip -> pip.removeOnIsInPipStateChangedListener(
+                        mOnIsInPipStateChangedListener));
 
                 try {
                     mWindowManagerService.unregisterSystemGestureExclusionListener(
@@ -720,7 +721,7 @@
                         mTaskStackListener);
                 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
                         mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener);
-                mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(
+                mPipOptional.ifPresent(pip -> pip.addOnIsInPipStateChangedListener(
                         mOnIsInPipStateChangedListener));
                 mDesktopModeOptional.ifPresent(
                         dm -> dm.addDesktopGestureExclusionRegionListener(
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index 8d48c1d..1cf4c23 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -26,11 +26,13 @@
 import com.android.systemui.power.data.repository.PowerRepository
 import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
@@ -56,7 +58,7 @@
      * Unless you need to respond differently to different [WakeSleepReason]s, you should use
      * [isAwake].
      */
-    val detailedWakefulness = repository.wakefulness
+    val detailedWakefulness: StateFlow<WakefulnessModel> = repository.wakefulness
 
     /**
      * Whether we're awake (screen is on and responding to user touch) or asleep (screen is off, or
@@ -189,9 +191,7 @@
      * In tests, you should be able to use [setAsleepForTest] rather than calling this method
      * directly.
      */
-    fun onFinishedGoingToSleep(
-        powerButtonLaunchGestureTriggeredDuringSleep: Boolean,
-    ) {
+    fun onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep: Boolean) {
         // If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry
         // that state forward. It will be reset by the next onStartedGoingToSleep.
         val powerButtonLaunchGestureTriggered =
@@ -255,7 +255,7 @@
         @JvmOverloads
         fun PowerInteractor.setAwakeForTest(
             @PowerManager.WakeReason reason: Int = PowerManager.WAKE_REASON_UNKNOWN,
-            forceEmit: Boolean = false
+            forceEmit: Boolean = false,
         ) {
             emitDuplicateWakefulnessValue = forceEmit
 
@@ -286,9 +286,7 @@
             emitDuplicateWakefulnessValue = forceEmit
 
             this.onStartedGoingToSleep(reason = sleepReason)
-            this.onFinishedGoingToSleep(
-                powerButtonLaunchGestureTriggeredDuringSleep = false,
-            )
+            this.onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep = false)
         }
 
         /** Helper method for tests to simulate the device screen state change event. */
diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
index 765b45b..bab88c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
@@ -159,7 +159,7 @@
      * Returns true if lock screen entry point for QR Code Scanner is to be enabled.
      */
     public boolean isEnabledForLockScreenButton() {
-        return mQRCodeScannerEnabled && isAbleToLaunchScannerActivity() && isAllowedOnLockScreen();
+        return isAbleToLaunchScannerActivity() && isAllowedOnLockScreen();
     }
 
     /** Returns whether the QR scanner button is allowed on lockscreen. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 624cf30..e912a0c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -63,6 +63,7 @@
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.asIndenting
+import com.android.systemui.util.kotlin.emitOnStart
 import com.android.systemui.util.printSection
 import com.android.systemui.util.println
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
@@ -278,6 +279,24 @@
     private val mediaSuddenlyAppearingInLandscape: Boolean
         get() = !qqsMediaInRow && qsMediaInRow
 
+    private val collapsedLandscapeMedia by
+        hydrator.hydratedStateOf(
+            traceName = "collapsedLandscapeMedia",
+            initialValue = resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed),
+            source =
+                configurationInteractor.onAnyConfigurationChange.emitOnStart().map {
+                    resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed)
+                },
+        )
+
+    private val qqsMediaExpansion: Float
+        get() =
+            if (qqsMediaInRow && collapsedLandscapeMedia) {
+                MediaHostState.COLLAPSED
+            } else {
+                MediaHostState.EXPANDED
+            }
+
     private var qsBounds by mutableStateOf(Rect())
 
     private val constrainedSquishinessFraction: Float
@@ -379,6 +398,7 @@
         initMediaHosts() // init regardless of using media (same as current QS).
         coroutineScope {
             launch { hydrateSquishinessInteractor() }
+            launch { hydrateQqsMediaExpansion() }
             launch { hydrator.activate() }
             launch { containerViewModel.activate() }
             launch { qqsMediaInRowViewModel.activate() }
@@ -389,7 +409,7 @@
 
     private fun initMediaHosts() {
         qqsMediaHost.apply {
-            expansion = MediaHostState.EXPANDED
+            expansion = qqsMediaExpansion
             showsOnlyActiveMedia = true
             init(MediaHierarchyManager.LOCATION_QQS)
         }
@@ -405,6 +425,10 @@
             .collect { squishinessInteractor.setSquishinessValue(it) }
     }
 
+    private suspend fun hydrateQqsMediaExpansion() {
+        snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it }
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.asIndenting().run {
             printSection("Quick Settings state") {
@@ -448,6 +472,8 @@
                 println("qqsMediaInRow", qqsMediaInRow)
                 println("qsMediaVisible", qsMediaVisible)
                 println("qsMediaInRow", qsMediaInRow)
+                println("collapsedLandscapeMedia", collapsedLandscapeMedia)
+                println("qqsMediaExpansion", qqsMediaExpansion)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
index 971598d..b0c6073 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
@@ -17,9 +17,16 @@
 package com.android.systemui.qs.panels.data.repository
 
 import android.content.Context
+import android.content.IntentFilter
 import android.content.SharedPreferences
+import com.android.systemui.backup.BackupHelper
+import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.qs.panels.shared.model.PanelsLog
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.settings.UserFileManager
 import com.android.systemui.user.data.repository.UserRepository
@@ -29,9 +36,11 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 
 /** Repository for QS user preferences. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -43,26 +52,31 @@
     private val userRepository: UserRepository,
     private val defaultLargeTilesRepository: DefaultLargeTilesRepository,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @PanelsLog private val logBuffer: LogBuffer,
+    broadcastDispatcher: BroadcastDispatcher,
 ) {
-    /** Whether to show the labels on icon tiles for the current user. */
-    val showLabels: Flow<Boolean> =
-        userRepository.selectedUserInfo
-            .flatMapLatest { userInfo ->
-                val prefs = getSharedPrefs(userInfo.id)
-                prefs.observe().emitOnStart().map { prefs.getBoolean(ICON_LABELS_KEY, false) }
-            }
-            .flowOn(backgroundDispatcher)
+    private val logger by lazy { Logger(logBuffer, TAG) }
+
+    private val backupRestorationEvents: Flow<Unit> =
+        broadcastDispatcher
+            .broadcastFlow(
+                filter = IntentFilter(ACTION_RESTORE_FINISHED),
+                flags = Context.RECEIVER_NOT_EXPORTED,
+                permission = BackupHelper.PERMISSION_SELF,
+            )
+            .onEach { logger.i("Restored state for QS preferences.") }
+            .emitOnStart()
 
     /** Set of [TileSpec] to display as large tiles for the current user. */
     val largeTilesSpecs: Flow<Set<TileSpec>> =
-        userRepository.selectedUserInfo
-            .flatMapLatest { userInfo ->
+        combine(backupRestorationEvents, userRepository.selectedUserInfo, ::Pair)
+            .flatMapLatest { (_, userInfo) ->
                 val prefs = getSharedPrefs(userInfo.id)
                 prefs.observe().emitOnStart().map {
                     prefs
                         .getStringSet(
                             LARGE_TILES_SPECS_KEY,
-                            defaultLargeTilesRepository.defaultLargeTiles.map { it.spec }.toSet()
+                            defaultLargeTilesRepository.defaultLargeTiles.map { it.spec }.toSet(),
                         )
                         ?.map { TileSpec.create(it) }
                         ?.toSet() ?: defaultLargeTilesRepository.defaultLargeTiles
@@ -70,13 +84,6 @@
             }
             .flowOn(backgroundDispatcher)
 
-    /** Sets for the current user whether to show the labels on icon tiles. */
-    fun setShowLabels(showLabels: Boolean) {
-        with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) {
-            edit().putBoolean(ICON_LABELS_KEY, showLabels).apply()
-        }
-    }
-
     /** Sets for the current user the set of [TileSpec] to display as large tiles. */
     fun setLargeTilesSpecs(specs: Set<TileSpec>) {
         with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) {
@@ -85,15 +92,11 @@
     }
 
     private fun getSharedPrefs(userId: Int): SharedPreferences {
-        return userFileManager.getSharedPreferences(
-            FILE_NAME,
-            Context.MODE_PRIVATE,
-            userId,
-        )
+        return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId)
     }
 
     companion object {
-        private const val ICON_LABELS_KEY = "show_icon_labels"
+        private const val TAG = "QSPreferencesRepository"
         private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs"
         const val FILE_NAME = "quick_settings_prefs"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.kt
new file mode 100644
index 0000000..2c51ca9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/backup/QSPreferencesBackupHelper.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.qs.panels.domain.backup
+
+import android.app.backup.SharedPreferencesBackupHelper
+import android.content.Context
+import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository.Companion.FILE_NAME
+import com.android.systemui.settings.UserFileManagerImpl
+
+class QSPreferencesBackupHelper(context: Context, userId: Int) :
+    SharedPreferencesBackupHelper(
+        context,
+        UserFileManagerImpl.createFile(userId = userId, fileName = FILE_NAME).path,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index d107222..4a51bf0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -33,7 +33,6 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicText
-import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -63,6 +62,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.modifiers.size
 import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
@@ -194,26 +194,35 @@
                 is Icon.Resource -> context.getDrawable(icon.res)
             }
         }
-    if (loadedDrawable !is Animatable) {
-        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
-    } else if (icon is Icon.Resource) {
-        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+    if (loadedDrawable is Animatable) {
         val painter =
-            key(icon) {
-                if (animateToEnd) {
-                    rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
-                } else {
-                    var atEnd by remember(icon.res) { mutableStateOf(false) }
-                    LaunchedEffect(key1 = icon.res) { atEnd = true }
-                    rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+            when (icon) {
+                is Icon.Resource -> {
+                    val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+                    key(icon) {
+                        if (animateToEnd) {
+                            rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+                        } else {
+                            var atEnd by remember(icon.res) { mutableStateOf(false) }
+                            LaunchedEffect(key1 = icon.res) { atEnd = true }
+                            rememberAnimatedVectorPainter(
+                                animatedImageVector = image,
+                                atEnd = atEnd,
+                            )
+                        }
+                    }
                 }
+                is Icon.Loaded -> rememberDrawablePainter(loadedDrawable)
             }
+
         Image(
             painter = painter,
             contentDescription = icon.contentDescription?.load(),
             colorFilter = ColorFilter.tint(color = animatedColor),
             modifier = iconModifier,
         )
+    } else {
+        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index f218d86..37d24de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -243,16 +243,16 @@
             }
             mSelectedCard = cards.get(selectedIndex);
             switch (mSelectedCard.getCardImage().getType()) {
-                case TYPE_URI:
-                case TYPE_URI_ADAPTIVE_BITMAP:
-                    mCardViewDrawable = null;
-                    break;
-                case TYPE_RESOURCE:
                 case TYPE_BITMAP:
                 case TYPE_ADAPTIVE_BITMAP:
-                case TYPE_DATA:
                     mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext);
                     break;
+                case TYPE_URI:
+                case TYPE_URI_ADAPTIVE_BITMAP:
+                case TYPE_RESOURCE:
+                case TYPE_DATA:
+                    mCardViewDrawable = null;
+                    break;
                 default:
                     Log.e(TAG, "Unknown icon type: " + mSelectedCard.getCardImage().getType());
                     mCardViewDrawable = null;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
index 42d4eff..63510b8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -20,6 +20,7 @@
 import android.content.res.Resources
 import android.view.LayoutInflater
 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import com.android.systemui.CoreStartable
 import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.common.ui.ConfigurationStateImpl
 import com.android.systemui.common.ui.GlobalConfig
@@ -29,12 +30,16 @@
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
+import com.android.systemui.shade.data.repository.ShadePositionRepository
+import com.android.systemui.shade.data.repository.ShadePositionRepositoryImpl
 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
 import com.android.systemui.statusbar.phone.ConfigurationForwarder
 import com.android.systemui.statusbar.policy.ConfigurationController
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 
 /**
  * Module responsible for managing display-specific components and resources for the notification
@@ -149,4 +154,25 @@
             configurationInteractor
         }
     }
+
+    @SysUISingleton
+    @Provides
+    @ShadeDisplayAware
+    fun provideShadePositionRepository(impl: ShadePositionRepositoryImpl): ShadePositionRepository {
+        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()
+        return impl
+    }
+
+    @Provides
+    @IntoMap
+    @ClassKey(ShadePositionRepositoryImpl::class)
+    fun provideShadePositionRepositoryAsCoreStartable(
+        impl: ShadePositionRepositoryImpl
+    ): CoreStartable {
+        return if (ShadeWindowGoesAround.isEnabled) {
+            impl
+        } else {
+            CoreStartable.NOP
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
new file mode 100644
index 0000000..802fc0e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+import android.view.Display
+import com.android.systemui.shade.data.repository.ShadePositionRepository
+import com.android.systemui.statusbar.commandline.Command
+import java.io.PrintWriter
+
+class ShadePrimaryDisplayCommand(private val positionRepository: ShadePositionRepository) :
+    Command {
+
+    override fun execute(pw: PrintWriter, args: List<String>) {
+        if (args[0].lowercase() == "reset") {
+            positionRepository.resetDisplayId()
+            pw.println("Reset shade primary display id to ${Display.DEFAULT_DISPLAY}")
+            return
+        }
+
+        val displayId: Int =
+            try {
+                args[0].toInt()
+            } catch (e: NumberFormatException) {
+                pw.println("Error: task id should be an integer")
+                return
+            }
+
+        if (displayId < 0) {
+            pw.println("Error: display id should be positive integer")
+        }
+
+        positionRepository.setDisplayId(displayId)
+        pw.println("New shade primary display id is $displayId")
+    }
+
+    override fun help(pw: PrintWriter) {
+        pw.println("shade_display_override <displayId> ")
+        pw.println("Set the display which is holding the shade.")
+        pw.println("shade_display_override reset ")
+        pw.println("Reset the display which is holding the shade.")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt
new file mode 100644
index 0000000..24c067a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadePositionRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadePrimaryDisplayCommand
+import com.android.systemui.statusbar.commandline.CommandRegistry
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+interface ShadePositionRepository {
+    /** ID of the display which currently hosts the shade */
+    val displayId: StateFlow<Int>
+
+    /**
+     * Updates the value of the shade display id stored, emitting to the new display id to every
+     * component dependent on the shade display id
+     */
+    fun setDisplayId(displayId: Int)
+
+    /** Resets value of shade primary display to the default display */
+    fun resetDisplayId()
+}
+
+/** Source of truth for the display currently holding the shade. */
+@SysUISingleton
+class ShadePositionRepositoryImpl
+@Inject
+constructor(private val commandRegistry: CommandRegistry) : ShadePositionRepository, CoreStartable {
+    private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY)
+
+    override val displayId: StateFlow<Int>
+        get() = _displayId
+
+    override fun setDisplayId(displayId: Int) {
+        _displayId.value = displayId
+    }
+
+    override fun resetDisplayId() {
+        _displayId.value = Display.DEFAULT_DISPLAY
+    }
+
+    override fun start() {
+        commandRegistry.registerCommand("shade_display_override") {
+            ShadePrimaryDisplayCommand(this)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 3a24ec9..c1b8d9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -383,34 +383,20 @@
         }
 
         protected boolean hasSameIcon(Object parentData, Object childData) {
-            Icon parentIcon = getIcon((Notification) parentData);
-            Icon childIcon = getIcon((Notification) childData);
+            Icon parentIcon = ((Notification) parentData).getSmallIcon();
+            Icon childIcon = ((Notification) childData).getSmallIcon();
             return parentIcon.sameAs(childIcon);
         }
 
-        private static Icon getIcon(Notification notification) {
-            if (notification.shouldUseAppIcon()) {
-                return notification.getAppIcon();
-            }
-            return notification.getSmallIcon();
-        }
-
         /**
          * @return whether two ImageViews have the same colorFilterSet or none at all
          */
         protected boolean hasSameColor(Object parentData, Object childData) {
-            int parentColor = getColor((Notification) parentData);
-            int childColor = getColor((Notification) childData);
+            int parentColor = ((Notification) parentData).color;
+            int childColor = ((Notification) childData).color;
             return parentColor == childColor;
         }
 
-        private static int getColor(Notification notification) {
-            if (notification.shouldUseAppIcon()) {
-                return 0;  // the color filter isn't applied if using the app icon
-            }
-            return notification.color;
-        }
-
         @Override
         public boolean isEmpty(View view) {
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index ad3afd4..33f0c64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -27,7 +27,6 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -36,7 +35,6 @@
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.Trace;
@@ -520,36 +518,10 @@
                 userId = UserHandle.USER_SYSTEM;
             }
 
-            // Try to load the monochrome app icon if applicable
-            Drawable icon = maybeGetMonochromeAppIcon(context, statusBarIcon);
-            // Otherwise, just use the icon normally
-            if (icon == null) {
-                icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
-            }
-            return icon;
+            return statusBarIcon.icon.loadDrawableAsUser(context, userId);
         }
     }
 
-    @Nullable
-    private Drawable maybeGetMonochromeAppIcon(Context context,
-            StatusBarIcon statusBarIcon) {
-        if (android.app.Flags.notificationsUseMonochromeAppIcon()
-                && statusBarIcon.type == StatusBarIcon.Type.MaybeMonochromeAppIcon) {
-            // Check if we have a monochrome app icon
-            PackageManager pm = context.getPackageManager();
-            Drawable appIcon = context.getApplicationInfo().loadIcon(pm);
-            if (appIcon instanceof AdaptiveIconDrawable) {
-                Drawable monochrome = ((AdaptiveIconDrawable) appIcon).getMonochrome();
-                if (monochrome != null) {
-                    setCropToPadding(true);
-                    setScaleType(ScaleType.CENTER);
-                    return new ScalingDrawableWrapper(monochrome, APP_ICON_SCALE);
-                }
-            }
-        }
-        return null;
-    }
-
     public StatusBarIcon getStatusBarIcon() {
         return mIcon;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 9c53cc1..e3dc70a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -28,14 +29,19 @@
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.launch
 
 /** Interactor for the screen recording chip shown in the status bar. */
 @SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
 class ScreenRecordChipInteractor
 @Inject
 constructor(
@@ -44,6 +50,32 @@
     private val mediaProjectionRepository: MediaProjectionRepository,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) {
+    /**
+     * Emits true if we should assume that we're currently screen recording, even if
+     * [ScreenRecordRepository.screenRecordState] hasn't emitted [ScreenRecordModel.Recording] yet.
+     */
+    private val shouldAssumeIsRecording: Flow<Boolean> =
+        screenRecordRepository.screenRecordState
+            .transformLatest {
+                when (it) {
+                    is ScreenRecordModel.DoingNothing -> {
+                        emit(false)
+                    }
+                    is ScreenRecordModel.Starting -> {
+                        // If we're told that the recording will start in [it.millisUntilStarted],
+                        // optimistically assume the recording did indeed start after that time even
+                        // if [ScreenRecordRepository.screenRecordState] hasn't emitted
+                        // [ScreenRecordModel.Recording] yet. Start 50ms early so that the chip
+                        // timer will definitely be showing by the time the recording actually
+                        // starts - see b/366448907.
+                        delay(it.millisUntilStarted - 50)
+                        emit(true)
+                    }
+                    is ScreenRecordModel.Recording -> {}
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     val screenRecordState: StateFlow<ScreenRecordChipModel> =
         // ScreenRecordRepository has the main "is the screen being recorded?" state, and
         // MediaProjectionRepository has information about what specifically is being recorded (a
@@ -51,37 +83,55 @@
         combine(
                 screenRecordRepository.screenRecordState,
                 mediaProjectionRepository.mediaProjectionState,
-            ) { screenRecordState, mediaProjectionState ->
-                when (screenRecordState) {
-                    is ScreenRecordModel.DoingNothing -> {
-                        logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" })
-                        ScreenRecordChipModel.DoingNothing
-                    }
-                    is ScreenRecordModel.Starting -> {
-                        logger.log(
-                            TAG,
-                            LogLevel.INFO,
-                            { long1 = screenRecordState.millisUntilStarted },
-                            { "State: Starting($long1)" }
-                        )
-                        ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
-                    }
-                    is ScreenRecordModel.Recording -> {
-                        val recordedTask =
-                            if (
-                                mediaProjectionState is MediaProjectionState.Projecting.SingleTask
-                            ) {
-                                mediaProjectionState.task
-                            } else {
-                                null
-                            }
-                        logger.log(
-                            TAG,
-                            LogLevel.INFO,
-                            { str1 = recordedTask?.baseIntent?.component?.packageName },
-                            { "State: Recording(taskPackage=$str1)" }
-                        )
-                        ScreenRecordChipModel.Recording(recordedTask)
+                shouldAssumeIsRecording,
+            ) { screenRecordState, mediaProjectionState, shouldAssumeIsRecording ->
+                if (
+                    Flags.statusBarAutoStartScreenRecordChip() &&
+                        shouldAssumeIsRecording &&
+                        screenRecordState is ScreenRecordModel.Starting
+                ) {
+                    logger.log(
+                        TAG,
+                        LogLevel.INFO,
+                        {},
+                        { "State: Recording(taskPackage=null) due to force-start" },
+                    )
+                    ScreenRecordChipModel.Recording(recordedTask = null)
+                } else {
+                    when (screenRecordState) {
+                        is ScreenRecordModel.DoingNothing -> {
+                            logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" })
+                            ScreenRecordChipModel.DoingNothing
+                        }
+
+                        is ScreenRecordModel.Starting -> {
+                            logger.log(
+                                TAG,
+                                LogLevel.INFO,
+                                { long1 = screenRecordState.millisUntilStarted },
+                                { "State: Starting($long1)" },
+                            )
+                            ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
+                        }
+
+                        is ScreenRecordModel.Recording -> {
+                            val recordedTask =
+                                if (
+                                    mediaProjectionState
+                                        is MediaProjectionState.Projecting.SingleTask
+                                ) {
+                                    mediaProjectionState.task
+                                } else {
+                                    null
+                                }
+                            logger.log(
+                                TAG,
+                                LogLevel.INFO,
+                                { str1 = recordedTask?.baseIntent?.component?.packageName },
+                                { "State: Recording(taskPackage=$str1)" },
+                            )
+                            ScreenRecordChipModel.Recording(recordedTask)
+                        }
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
index 3a31851..52a79d3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.connectivity;
 
+import static com.android.systemui.Flags.multiuserWifiPickerTrackerSupport;
+
+import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -65,13 +68,16 @@
     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
 
     private int mCurrentUser;
+    private Context mContext;
 
     public AccessPointControllerImpl(
+            Context context,
             UserManager userManager,
             UserTracker userTracker,
             Executor mainExecutor,
             WifiPickerTrackerFactory wifiPickerTrackerFactory
     ) {
+        mContext = context;
         mUserManager = userManager;
         mUserTracker = userTracker;
         mCurrentUser = userTracker.getUserId();
@@ -87,7 +93,11 @@
      */
     public void init() {
         if (mWifiPickerTracker == null) {
-            mWifiPickerTracker = mWifiPickerTrackerFactory.create(this.getLifecycle(), this, TAG);
+            // We are creating the WifiPickerTracker during init to make sure we have one
+            // available at all times however we expect this to be recreated very quickly
+            // with a user-specific context in onUserSwitched.
+            mWifiPickerTracker =
+                mWifiPickerTrackerFactory.create(mContext, this.getLifecycle(), this, TAG);
         }
     }
 
@@ -116,6 +126,19 @@
 
     void onUserSwitched(int newUserId) {
         mCurrentUser = newUserId;
+        // Return early if multiuser support is not enabled.
+        if (!multiuserWifiPickerTrackerSupport()) {
+            return;
+        }
+
+        if (mWifiPickerTracker != null) {
+            mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.CREATED));
+        }
+        Context context = mContext.createContextAsUser(UserHandle.of(newUserId), /* flags= */ 0);
+        mWifiPickerTracker = mWifiPickerTrackerFactory.create(context, mLifecycle, this, TAG);
+        if (!mCallbacks.isEmpty()) {
+            mMainExecutor.execute(() -> mLifecycle.setCurrentState(Lifecycle.State.STARTED));
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt
index dc2ebe5..947ce33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt
@@ -22,6 +22,7 @@
 import android.os.Handler
 import android.os.SimpleClock
 import androidx.lifecycle.Lifecycle
+import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.util.concurrency.ThreadFactory
@@ -41,7 +42,7 @@
 class WifiPickerTrackerFactory
 @Inject
 constructor(
-    private val context: Context,
+    private val applicationContext: Context,
     private val wifiManager: WifiManager?,
     private val connectivityManager: ConnectivityManager,
     private val systemClock: SystemClock,
@@ -64,16 +65,23 @@
      * @return a new [WifiPickerTracker] or null if [WifiManager] is null.
      */
     fun create(
+        userContext: Context,
         lifecycle: Lifecycle,
         listener: WifiPickerTrackerCallback,
         name: String,
     ): WifiPickerTracker? {
         return if (wifiManager == null) {
             null
-        } else
+        } else {
+            val contextToUse =
+                if (multiuserWifiPickerTrackerSupport()) {
+                    userContext
+                } else {
+                    applicationContext
+                }
             WifiPickerTracker(
                 lifecycle,
-                context,
+                contextToUse,
                 wifiManager,
                 connectivityManager,
                 mainHandler,
@@ -86,6 +94,7 @@
                 SCAN_INTERVAL_MILLIS,
                 listener,
             )
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
index f441fd6..4c54fc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt
@@ -112,12 +112,12 @@
     }
 
     override fun initializeStatusBar() {
-        StatusBarSimpleFragment.assertInLegacyMode()
+        StatusBarRootModernization.assertInLegacyMode()
         doStart()
     }
 
     private fun doStart() {
-        if (StatusBarSimpleFragment.isEnabled) doComposeStart() else doLegacyStart()
+        if (StatusBarRootModernization.isEnabled) doComposeStart() else doLegacyStart()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
index f33b76b..ff4760f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
@@ -18,8 +18,10 @@
 
 import android.view.Display
 import android.view.View
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.CoreStartable
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.DarkIconDispatcher
@@ -46,12 +48,12 @@
 import dagger.assisted.AssistedInject
 import java.io.PrintWriter
 import java.util.Optional
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.filterNotNull
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Class responsible for managing the lifecycle and state of the status bar.
@@ -68,6 +70,7 @@
     @Assisted private val statusBarModeRepository: StatusBarModePerDisplayRepository,
     @Assisted private val statusBarInitializer: StatusBarInitializer,
     @Assisted private val statusBarWindowController: StatusBarWindowController,
+    @Main private val mainContext: CoroutineContext,
     private val demoModeController: DemoModeController,
     private val pluginDependencyProvider: PluginDependencyProvider,
     private val autoHideController: AutoHideController,
@@ -141,7 +144,8 @@
     override fun start() {
         StatusBarConnectedDisplays.assertInNewMode()
         coroutineScope
-            .launch {
+            // Perform animations on the main thread to prevent crashes.
+            .launch(context = mainContext) {
                 dumpManager.registerCriticalDumpable(dumpableName, this@StatusBarOrchestrator)
                 launch {
                     controllerAndBouncerShowing.collect { (controller, bouncerShowing) ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarSimpleFragment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarSimpleFragment.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt
index 2141513..057213f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarSimpleFragment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt
@@ -21,9 +21,9 @@
 import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading and using the status bar simple fragment flag state */
-object StatusBarSimpleFragment {
+object StatusBarRootModernization {
     /** Aconfig flag for removing the fragment */
-    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT
+    const val FLAG_NAME = Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION
 
     /** A token used for dependency declaration */
     val token: FlagToken
@@ -32,7 +32,7 @@
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.statusBarSimpleFragment()
+        get() = Flags.statusBarRootModernization()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index 2fded34..e232849 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint
 import android.app.NotificationManager
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
@@ -50,7 +51,6 @@
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * If the setting is enabled, this will track seen notifications and ensure that they only show in
@@ -74,7 +74,7 @@
 
     private val unseenNotifications = mutableSetOf<NotificationEntry>()
     private var isShadeVisible = false
-    private var unseenFilterEnabled = false
+    private var minimalismEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
         if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) {
@@ -83,7 +83,7 @@
         pipeline.addPromoter(unseenNotifPromoter)
         pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
         pipeline.addCollectionListener(collectionListener)
-        scope.launch { trackUnseenFilterSettingChanges() }
+        scope.launch { trackLockScreenNotificationMinimalismSettingChanges() }
         dumpManager.registerDumpable(this)
     }
 
@@ -136,12 +136,12 @@
         return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled()
     }
 
-    private suspend fun trackUnseenFilterSettingChanges() {
+    private suspend fun trackLockScreenNotificationMinimalismSettingChanges() {
         // Only filter the seen notifs when the lock screen minimalism feature settings is on.
         minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled ->
             // update local field and invalidate if necessary
-            if (isMinimalismSettingEnabled != unseenFilterEnabled) {
-                unseenFilterEnabled = isMinimalismSettingEnabled
+            if (isMinimalismSettingEnabled != minimalismEnabled) {
+                minimalismEnabled = isMinimalismSettingEnabled
                 unseenNotifications.clear()
                 unseenNotifPromoter.invalidateList("unseen setting changed")
             }
@@ -156,21 +156,21 @@
     private val collectionListener =
         object : NotifCollectionListener {
             override fun onEntryAdded(entry: NotificationEntry) {
-                if (unseenFilterEnabled && !isShadeVisible) {
+                if (minimalismEnabled && !isShadeVisible) {
                     logger.logUnseenAdded(entry.key)
                     unseenNotifications.add(entry)
                 }
             }
 
             override fun onEntryUpdated(entry: NotificationEntry) {
-                if (unseenFilterEnabled && !isShadeVisible) {
+                if (minimalismEnabled && !isShadeVisible) {
                     logger.logUnseenUpdated(entry.key)
                     unseenNotifications.add(entry)
                 }
             }
 
             override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-                if (unseenFilterEnabled && unseenNotifications.remove(entry)) {
+                if (minimalismEnabled && unseenNotifications.remove(entry)) {
                     logger.logUnseenRemoved(entry.key)
                 }
             }
@@ -178,7 +178,7 @@
 
     private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
         if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
-        if (!unseenFilterEnabled) return
+        if (!minimalismEnabled) return
         // Only ever elevate a top unseen notification on keyguard, not even locked shade
         if (statusBarStateController.state != StatusBarState.KEYGUARD) {
             seenNotificationsInteractor.setTopOngoingNotification(null)
@@ -215,6 +215,7 @@
             override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
                 when {
                     NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false
+                    !minimalismEnabled -> false
                     seenNotificationsInteractor.isTopOngoingNotification(child) -> true
                     !NotificationMinimalism.ungroupTopUnseen -> false
                     else -> seenNotificationsInteractor.isTopUnseenNotification(child)
@@ -225,6 +226,7 @@
         object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
             override fun isInSection(entry: ListEntry): Boolean {
                 if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
+                if (!minimalismEnabled) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
                 }
@@ -235,6 +237,7 @@
         object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
             override fun isInSection(entry: ListEntry): Boolean {
                 if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
+                if (!minimalismEnabled) return false
                 return entry.anyEntry { notificationEntry ->
                     seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
index 16d0cc4..3c8c42f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
@@ -18,7 +18,6 @@
 
 import android.app.Notification
 import android.content.Context
-import android.graphics.drawable.Drawable
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.contentDescForNotification
@@ -30,15 +29,11 @@
         return StatusBarIconView(
             context,
             "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}",
-            entry.sbn
+            entry.sbn,
         )
     }
 
     fun getIconContentDescription(n: Notification): CharSequence {
         return contentDescForNotification(context, n)
     }
-
-    fun getAppIcon(n: Notification): Drawable {
-        return n.loadHeaderAppIcon(context)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index db80483..4717194 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -26,6 +26,7 @@
 import android.util.Log
 import android.view.View
 import android.widget.ImageView
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.app.tracing.traceSection
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.Flags
@@ -44,7 +45,6 @@
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.withContext
 
 /**
@@ -152,13 +152,7 @@
                 setIcon(entry, sensitiveIconDescriptor, shelfIcon)
                 setIcon(entry, sensitiveIconDescriptor, aodIcon)
                 entry.icons =
-                    IconPack.buildPack(
-                        sbIcon,
-                        sbChipIcon,
-                        shelfIcon,
-                        aodIcon,
-                        entry.icons,
-                    )
+                    IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons)
             } catch (e: InflationException) {
                 entry.icons = IconPack.buildEmptyPack(entry.icons)
                 throw e
@@ -182,7 +176,7 @@
                 Log.wtf(
                     TAG,
                     "Updating using the cache is not supported when the " +
-                        "notifications_background_icons flag is off"
+                        "notifications_background_icons flag is off",
                 )
             }
             if (!usingCache || !Flags.notificationsBackgroundIcons()) {
@@ -249,10 +243,6 @@
         val (icon: Icon?, type: StatusBarIcon.Type) =
             if (showPeopleAvatar) {
                 createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar
-            } else if (
-                android.app.Flags.notificationsUseMonochromeAppIcon() && n.shouldUseAppIcon()
-            ) {
-                n.smallIcon to StatusBarIcon.Type.MaybeMonochromeAppIcon
             } else {
                 n.smallIcon to StatusBarIcon.Type.NotifSmallIcon
             }
@@ -267,33 +257,25 @@
 
     private fun getCachedIconDescriptor(
         entry: NotificationEntry,
-        showPeopleAvatar: Boolean
+        showPeopleAvatar: Boolean,
     ): StatusBarIcon? {
         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
-        val appIconDescriptor = entry.icons.appIconDescriptor
         val smallIconDescriptor = entry.icons.smallIconDescriptor
 
         // If cached, return corresponding cached values
         return when {
             showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor
-            android.app.Flags.notificationsUseMonochromeAppIcon() && appIconDescriptor != null ->
-                appIconDescriptor
             smallIconDescriptor != null -> smallIconDescriptor
             else -> null
         }
     }
 
     private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) {
-        if (
-            android.app.Flags.notificationsUseAppIcon() ||
-                android.app.Flags.notificationsUseMonochromeAppIcon()
-        ) {
-            // If either of the new icon flags is enabled, we cache the icon all the time.
+        if (android.app.Flags.notificationsRedesignAppIcons()) {
+            // Although we're not actually using the app icon in the status bar, let's make sure
+            // we cache the icon all the time when the flag is on.
             when (descriptor.type) {
                 StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor
-                // When notificationsUseMonochromeAppIcon is enabled, we use the appIconDescriptor.
-                StatusBarIcon.Type.MaybeMonochromeAppIcon ->
-                    entry.icons.appIconDescriptor = descriptor
                 // When notificationsUseAppIcon is enabled, the app icon overrides the small icon.
                 // But either way, it's a good idea to cache the descriptor.
                 else -> entry.icons.smallIconDescriptor = descriptor
@@ -312,7 +294,7 @@
     private fun setIcon(
         entry: NotificationEntry,
         iconDescriptor: StatusBarIcon,
-        iconView: StatusBarIconView
+        iconView: StatusBarIconView,
     ) {
         iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
         iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
@@ -323,7 +305,7 @@
 
     private fun Icon.toStatusBarIcon(
         entry: NotificationEntry,
-        type: StatusBarIcon.Type
+        type: StatusBarIcon.Type,
     ): StatusBarIcon {
         val n = entry.sbn.notification
         return StatusBarIcon(
@@ -333,7 +315,7 @@
             n.iconLevel,
             n.number,
             iconBuilder.getIconContentDescription(n),
-            type
+            type,
         )
     }
 
@@ -347,7 +329,7 @@
                 } catch (e: Exception) {
                     Log.e(
                         TAG,
-                        "Error calling LauncherApps#getShortcutIcon for notification $entry: $e"
+                        "Error calling LauncherApps#getShortcutIcon for notification $entry: $e",
                     )
                 }
             }
@@ -431,7 +413,7 @@
     private fun showsConversation(
         entry: NotificationEntry,
         iconView: StatusBarIconView,
-        iconDescriptor: StatusBarIcon
+        iconDescriptor: StatusBarIcon,
     ): Boolean {
         val usedInSensitiveContext =
             iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
index 611cebc..cb6be66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
@@ -34,7 +34,6 @@
     @Nullable private final StatusBarIconView mAodIcon;
 
     @Nullable private StatusBarIcon mSmallIconDescriptor;
-    @Nullable private StatusBarIcon mAppIconDescriptor;
     @Nullable private StatusBarIcon mPeopleAvatarDescriptor;
 
     private boolean mIsImportantConversation;
@@ -127,15 +126,6 @@
         mPeopleAvatarDescriptor = peopleAvatarDescriptor;
     }
 
-    @Nullable
-    StatusBarIcon getAppIconDescriptor() {
-        return mAppIconDescriptor;
-    }
-
-    void setAppIconDescriptor(@Nullable StatusBarIcon appIconDescriptor) {
-        mAppIconDescriptor = appIconDescriptor;
-    }
-
     boolean isImportantConversation() {
         return mIsImportantConversation;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt
index 6b5642a..83f56a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt
@@ -20,7 +20,6 @@
 import android.view.View
 import com.android.app.tracing.traceSection
 import com.android.internal.util.ContrastColorUtil
-import com.android.systemui.Flags
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.NO_COLOR
@@ -36,11 +35,9 @@
 
     suspend fun bindColor(view: StatusBarIconView, color: Flow<Int>) {
         color.collectTracingEach("SBIV#bindColor") { color ->
-            // Don't change the icon color if an app icon experiment is enabled.
-            if (!android.app.Flags.notificationsUseAppIcon()) {
-                view.staticDrawableColor = color
-            }
-            // Continue changing the overflow dot color
+            // Set the color for the icons
+            view.staticDrawableColor = color
+            // Set the color for the overflow dot
             view.setDecorColor(color)
         }
     }
@@ -59,14 +56,12 @@
         contrastColorUtil: ContrastColorUtil,
     ) {
         iconColors.collectTracingEach("SBIV#bindIconColors") { colors ->
-            // Don't change the icon color if an app icon experiment is enabled.
-            if (!android.app.Flags.notificationsUseAppIcon()) {
-                val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L)
-                val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil)
-                view.staticDrawableColor =
-                    if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR
-            }
-            // Continue changing the overflow dot color
+            // Set the icon color
+            val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L)
+            val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil)
+            view.staticDrawableColor =
+                if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR
+            // Set the color for the overflow dot
             view.setDecorColor(colors.tint)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index f352123..b622def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -203,11 +203,7 @@
         addRemainingTransformTypes();
         updateCropToPaddingForImageViews();
         Notification n = row.getEntry().getSbn().getNotification();
-        if (n.shouldUseAppIcon()) {
-            mIcon.setTag(ImageTransformState.ICON_TAG, n.getAppIcon());
-        } else {
-            mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon());
-        }
+        mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon());
 
         // We need to reset all views that are no longer transforming in case a view was previously
         // transformed, but now we decided to transform its container instead.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 3bc5495..5dff812 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -21,12 +21,14 @@
 import android.view.View.GONE
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
@@ -38,6 +40,9 @@
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.properties.Delegates.notNull
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
 
 private const val TAG = "NotifStackSizeCalc"
 private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)
@@ -56,7 +61,9 @@
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
     private val mediaDataManager: MediaDataManager,
     @Main private val resources: Resources,
-    private val splitShadeStateController: SplitShadeStateController
+    private val splitShadeStateController: SplitShadeStateController,
+    private val seenNotificationsInteractor: SeenNotificationsInteractor,
+    @Application private val scope: CoroutineScope,
 ) {
 
     /**
@@ -74,7 +81,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalism.isEnabled
+        get() = NotificationMinimalism.isEnabled && minimalismSettingEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -85,8 +92,14 @@
      */
     private var saveSpaceOnLockscreen = false
 
+    /** True when the lock screen notification minimalism feature setting is enabled */
+    private var minimalismSettingEnabled = false
+
     init {
         updateResources()
+        if (NotificationMinimalism.isEnabled) {
+            scope.launch { trackLockScreenNotificationMinimalismSettingChanges() }
+        }
     }
 
     private fun allowedByPolicy(stackHeight: StackHeight): Boolean =
@@ -199,7 +212,7 @@
                     canStackFitInSpace(
                         heightResult,
                         notifSpace = notifSpace,
-                        shelfSpace = shelfSpace
+                        shelfSpace = shelfSpace,
                     ) == FitResult.FIT
             }
 
@@ -229,7 +242,7 @@
                         canStackFitInSpace(
                             heightResult,
                             notifSpace = notifSpace,
-                            shelfSpace = shelfSpace
+                            shelfSpace = shelfSpace,
                         ) != FitResult.NO_FIT
                 }
             log { "\t--- maxNotifications=$maxNotifications" }
@@ -277,7 +290,7 @@
     fun computeHeight(
         stack: NotificationStackScrollLayout,
         maxNotifs: Int,
-        shelfHeight: Float
+        shelfHeight: Float,
     ): Float {
         log { "\n" }
         log { "computeHeight ---" }
@@ -311,7 +324,7 @@
     private enum class FitResult {
         FIT,
         FIT_IF_SAVE_SPACE,
-        NO_FIT
+        NO_FIT,
     }
 
     data class SpaceNeeded(
@@ -319,7 +332,7 @@
         val whenEnoughSpace: Float,
 
         // Float height of space needed when showing collapsed layout for FSI HUNs.
-        val whenSavingSpace: Float
+        val whenSavingSpace: Float,
     )
 
     private data class StackHeight(
@@ -335,9 +348,19 @@
         val shelfHeightWithSpaceBefore: Float,
 
         /** Whether the stack should actually be forced into the shelf before this height. */
-        val shouldForceIntoShelf: Boolean
+        val shouldForceIntoShelf: Boolean,
     )
 
+    private suspend fun trackLockScreenNotificationMinimalismSettingChanges() {
+        if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
+        seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled().collectLatest {
+            if (it != minimalismSettingEnabled) {
+                minimalismSettingEnabled = it
+            }
+            Log.i(TAG, "minimalismSettingEnabled: $minimalismSettingEnabled")
+        }
+    }
+
     private fun computeHeightPerNotificationLimit(
         stack: NotificationStackScrollLayout,
         shelfHeight: Float,
@@ -377,7 +400,7 @@
                             stack,
                             previous = currentNotification,
                             current = children[firstViewInShelfIndex],
-                            currentIndex = firstViewInShelfIndex
+                            currentIndex = firstViewInShelfIndex,
                         )
                     spaceBeforeShelf + shelfHeight
                 }
@@ -390,14 +413,15 @@
             log {
                 "\tcomputeHeightPerNotificationLimit i=$i notifs=$notifications " +
                     "notifsHeightSavingSpace=$notifsWithCollapsedHun" +
-                    " shelfWithSpaceBefore=$shelfWithSpaceBefore"
+                    " shelfWithSpaceBefore=$shelfWithSpaceBefore" +
+                    " limitLockScreenToOneImportant: $limitLockScreenToOneImportant"
             }
             yield(
                 StackHeight(
                     notifsHeight = notifications,
                     notifsHeightSavingSpace = notifsWithCollapsedHun,
                     shelfHeightWithSpaceBefore = shelfWithSpaceBefore,
-                    shouldForceIntoShelf = counter?.shouldForceIntoShelf() ?: false
+                    shouldForceIntoShelf = counter?.shouldForceIntoShelf() ?: false,
                 )
             )
         }
@@ -462,6 +486,10 @@
 
     fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("NotificationStackSizeCalculator saveSpaceOnLockscreen=$saveSpaceOnLockscreen")
+        pw.println(
+            "NotificationStackSizeCalculator " +
+                "limitLockScreenToOneImportant=$limitLockScreenToOneImportant"
+        )
     }
 
     private fun ExpandableView.isShowable(onLockscreen: Boolean): Boolean {
@@ -484,7 +512,7 @@
         stack: NotificationStackScrollLayout,
         previous: ExpandableView?,
         current: ExpandableView?,
-        currentIndex: Int
+        currentIndex: Int,
     ): Float {
         if (currentIndex == 0) {
             return 0f
@@ -536,11 +564,7 @@
         takeWhile(predicate).count() - 1
 
     /** Counts the number of notifications for each type of bucket */
-    data class BucketTypeCounter(
-        var ongoing: Int = 0,
-        var important: Int = 0,
-        var other: Int = 0,
-    ) {
+    data class BucketTypeCounter(var ongoing: Int = 0, var important: Int = 0, var other: Int = 0) {
         fun incrementForBucket(@PriorityBucket bucket: Int?) {
             when (bucket) {
                 BUCKET_MEDIA_CONTROLS,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 7389086..80c8e8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -203,7 +203,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
 import com.android.systemui.statusbar.core.StatusBarInitializer;
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment;
+import com.android.systemui.statusbar.core.StatusBarRootModernization;
 import com.android.systemui.statusbar.data.model.StatusBarMode;
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -1231,7 +1231,7 @@
                         checkBarModes();
                     });
         }
-        if (!StatusBarSimpleFragment.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) {
+        if (!StatusBarRootModernization.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) {
             // When the flag is on, we register the fragment as a core startable and this is not
             // needed
             mStatusBarInitializer.initializeStatusBar();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
index ba878ed..58386b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.statusbar.core.StatusBarInitializerImpl
 import com.android.systemui.statusbar.core.StatusBarInitializerStore
 import com.android.systemui.statusbar.core.StatusBarOrchestrator
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment
+import com.android.systemui.statusbar.core.StatusBarRootModernization
 import com.android.systemui.statusbar.data.repository.PrivacyDotViewControllerStoreModule
 import com.android.systemui.statusbar.data.repository.PrivacyDotWindowControllerStoreModule
 import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
@@ -87,7 +87,7 @@
             return if (StatusBarConnectedDisplays.isEnabled) {
                 // Will be started through MultiDisplayStatusBarStarter
                 CoreStartable.NOP
-            } else if (StatusBarSimpleFragment.isEnabled) {
+            } else if (StatusBarRootModernization.isEnabled) {
                 defaultInitializerLazy.get()
             } else {
                 // Will be started through CentralSurfaces
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 5cc4476..c55a63c 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
@@ -58,7 +58,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment;
+import com.android.systemui.statusbar.core.StatusBarRootModernization;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
@@ -365,7 +365,7 @@
         mPrimaryOngoingActivityChip = mStatusBar.findViewById(R.id.ongoing_activity_chip_primary);
         mSecondaryOngoingActivityChip =
                 mStatusBar.findViewById(R.id.ongoing_activity_chip_secondary);
-        if (!StatusBarSimpleFragment.isEnabled()) {
+        if (!StatusBarRootModernization.isEnabled()) {
             showEndSideContent(false);
             showClock(false);
         }
@@ -464,7 +464,7 @@
         super.onPause();
         mCommandQueue.removeCallback(this);
         mStatusBarStateController.removeCallback(this);
-        if (!StatusBarSimpleFragment.isEnabled()) {
+        if (!StatusBarRootModernization.isEnabled()) {
             mOngoingCallController.removeCallback(mOngoingCallListener);
         }
         mAnimationScheduler.removeCallback(this);
@@ -507,7 +507,7 @@
             mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons, displayId);
         }
 
-        if (!StatusBarSimpleFragment.isEnabled()) {
+        if (!StatusBarRootModernization.isEnabled()) {
             updateNotificationIconAreaAndOngoingActivityChip(/* animate= */ false);
         }
         Trace.endSection();
@@ -528,7 +528,7 @@
             new StatusBarVisibilityChangeListener() {
                 @Override
                 public void onStatusBarVisibilityMaybeChanged() {
-                    if (StatusBarSimpleFragment.isEnabled()) {
+                    if (StatusBarRootModernization.isEnabled()) {
                         return;
                     }
                     updateStatusBarVisibilities(/* animate= */ true);
@@ -536,7 +536,7 @@
 
                 @Override
                 public void onTransitionFromLockscreenToDreamStarted() {
-                    if (StatusBarSimpleFragment.isEnabled()) {
+                    if (StatusBarRootModernization.isEnabled()) {
                         return;
                     }
                     mTransitionFromLockscreenToDreamStarted = true;
@@ -547,7 +547,7 @@
                         boolean hasPrimaryOngoingActivity,
                         boolean hasSecondaryOngoingActivity,
                         boolean shouldAnimate) {
-                    if (StatusBarSimpleFragment.isEnabled()) {
+                    if (StatusBarRootModernization.isEnabled()) {
                         return;
                     }
                     mHasPrimaryOngoingActivity = hasPrimaryOngoingActivity;
@@ -558,7 +558,7 @@
                 @Override
                 public void onIsHomeStatusBarAllowedBySceneChanged(
                         boolean isHomeStatusBarAllowedByScene) {
-                    if (StatusBarSimpleFragment.isEnabled()) {
+                    if (StatusBarRootModernization.isEnabled()) {
                         return;
                     }
                     mHomeStatusBarAllowedByScene = isHomeStatusBarAllowedByScene;
@@ -568,7 +568,7 @@
 
     @Override
     public void disable(int displayId, int state1, int state2, boolean animate) {
-        if (StatusBarSimpleFragment.isEnabled()) {
+        if (StatusBarRootModernization.isEnabled()) {
             return;
         }
         if (displayId != getContext().getDisplayId()) {
@@ -582,7 +582,7 @@
     }
 
     private void updateStatusBarVisibilities(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
 
         StatusBarVisibilityModel previousModel = mLastModifiedVisibility;
         StatusBarVisibilityModel newModel = calculateInternalModel(mLastSystemVisibility);
@@ -623,7 +623,7 @@
 
     private StatusBarVisibilityModel calculateInternalModel(
             StatusBarVisibilityModel externalModel) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
 
         // TODO(b/328393714) use HeadsUpNotificationInteractor.showHeadsUpStatusBar instead.
         boolean headsUpVisible =
@@ -677,7 +677,7 @@
      * mLastModifiedVisibility.
      */
     private void updateNotificationIconAreaAndOngoingActivityChip(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
 
         StatusBarVisibilityModel visibilityModel = mLastModifiedVisibility;
         boolean disableNotifications = !visibilityModel.getShowNotificationIcons();
@@ -714,7 +714,7 @@
     }
 
     private boolean shouldHideStatusBar() {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
 
         boolean isDefaultDisplay = getContext().getDisplayId() == Display.DEFAULT_DISPLAY;
         boolean shouldHideForCurrentDisplay =
@@ -776,7 +776,7 @@
     }
 
     private void hideEndSideContent(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         if (!animate || !mAnimationsEnabled) {
             mEndSideAlphaController.setAlpha(/*alpha*/ 0f, SOURCE_OTHER);
         } else {
@@ -786,7 +786,7 @@
     }
 
     private void showEndSideContent(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         if (!animate || !mAnimationsEnabled) {
             mEndSideAlphaController.setAlpha(1f, SOURCE_OTHER);
             return;
@@ -803,18 +803,18 @@
     }
 
     private void hideClock(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateHiddenState(mClockView, clockHiddenMode(), animate);
     }
 
     private void showClock(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateShow(mClockView, animate);
     }
 
     /** Hides the primary ongoing activity chip. */
     private void hidePrimaryOngoingActivityChip(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateHiddenState(mPrimaryOngoingActivityChip, View.GONE, animate);
     }
 
@@ -826,18 +826,18 @@
      * activities. See b/332662551.
      */
     private void showPrimaryOngoingActivityChip(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateShow(mPrimaryOngoingActivityChip, animate);
     }
 
     private void hideSecondaryOngoingActivityChip(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateHiddenState(mSecondaryOngoingActivityChip, View.GONE, animate);
     }
 
     private void showSecondaryOngoingActivityChip(boolean animate) {
         StatusBarNotifChips.assertInNewMode();
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateShow(mSecondaryOngoingActivityChip, animate);
     }
 
@@ -846,7 +846,7 @@
      * don't set the clock GONE otherwise it'll mess up the animation.
      */
     private int clockHiddenMode() {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         if (!mShadeExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
                 && !mStatusBarStateController.isDozing()) {
             return View.INVISIBLE;
@@ -855,24 +855,24 @@
     }
 
     public void hideNotificationIconArea(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateHide(mNotificationIconAreaInner, animate);
     }
 
     public void showNotificationIconArea(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateShow(mNotificationIconAreaInner, animate);
     }
 
     public void hideOperatorName(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         if (mOperatorNameViewController != null) {
             animateHide(mOperatorNameViewController.getView(), animate);
         }
     }
 
     public void showOperatorName(boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         if (mOperatorNameViewController != null) {
             animateShow(mOperatorNameViewController.getView(), animate);
         }
@@ -882,7 +882,7 @@
      * Animate a view to INVISIBLE or GONE
      */
     private void animateHiddenState(final View v, int state, boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         v.animate().cancel();
         if (!animate || !mAnimationsEnabled) {
             v.setAlpha(0f);
@@ -902,7 +902,7 @@
      * Hides a view.
      */
     private void animateHide(final View v, boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         animateHiddenState(v, View.INVISIBLE, animate);
     }
 
@@ -910,7 +910,7 @@
      * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable.
      */
     private void animateShow(View v, boolean animate) {
-        StatusBarSimpleFragment.assertInLegacyMode();
+        StatusBarRootModernization.assertInLegacyMode();
         v.animate().cancel();
         v.setVisibility(View.VISIBLE);
         if (!animate || !mAnimationsEnabled) {
@@ -949,7 +949,7 @@
             mOperatorNameViewController.init();
             // This view should not be visible on lock-screen
             if (mKeyguardStateController.isShowing()) {
-                if (!StatusBarSimpleFragment.isEnabled()) {
+                if (!StatusBarRootModernization.isEnabled()) {
                     hideOperatorName(false);
                 }
             }
@@ -957,7 +957,7 @@
     }
 
     private void initOngoingCallChip() {
-        if (!StatusBarSimpleFragment.isEnabled()) {
+        if (!StatusBarRootModernization.isEnabled()) {
             mOngoingCallController.addCallback(mOngoingCallListener);
         }
         // TODO(b/364653005): Do we also need to set the secondary activity chip?
@@ -969,7 +969,7 @@
 
     @Override
     public void onDozingChanged(boolean isDozing) {
-        if (StatusBarSimpleFragment.isEnabled()) {
+        if (StatusBarRootModernization.isEnabled()) {
             return;
         }
         updateStatusBarVisibilities(/* animate= */ false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt
index eaf15a8..e4929b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaController.kt
@@ -20,7 +20,7 @@
 import androidx.core.animation.Interpolator
 import androidx.core.animation.ValueAnimator
 import com.android.app.animation.InterpolatorsAndroidX
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment
+import com.android.systemui.statusbar.core.StatusBarRootModernization
 
 /**
  * A controller that keeps track of multiple sources applying alpha value changes to a view. It will
@@ -75,7 +75,7 @@
 
     private fun applyAlphaToView() {
         val minAlpha = getMinAlpha()
-        if (!StatusBarSimpleFragment.isEnabled) {
+        if (!StatusBarRootModernization.isEnabled) {
             view.visibility = if (minAlpha != 0f) View.VISIBLE else View.INVISIBLE
             view.alpha = minAlpha
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
index 9cbfc44..94e9d26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
@@ -21,6 +21,7 @@
 import android.telephony.SignalStrength
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import android.telephony.satellite.NtnSignalStrength
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.dagger.SysUISingleton
@@ -31,11 +32,7 @@
 
 /** Logs for inputs into the mobile pipeline. */
 @SysUISingleton
-class MobileInputLogger
-@Inject
-constructor(
-    @MobileInputLog private val buffer: LogBuffer,
-) {
+class MobileInputLogger @Inject constructor(@MobileInputLog private val buffer: LogBuffer) {
     fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
         buffer.log(
             TAG,
@@ -49,7 +46,7 @@
             {
                 "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
                     " operator=$str1"
-            }
+            },
         )
     }
 
@@ -61,7 +58,7 @@
                 int1 = subId
                 bool1 = serviceState.isEmergencyOnly
             },
-            { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" }
+            { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" },
         )
     }
 
@@ -70,7 +67,7 @@
             TAG,
             LogLevel.INFO,
             { int1 = subId },
-            { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" }
+            { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" },
         )
     }
 
@@ -82,7 +79,16 @@
                 int1 = subId
                 str1 = signalStrength.toString()
             },
-            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
+            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" },
+        )
+    }
+
+    fun logNtnSignalStrengthChanged(signalStrength: NtnSignalStrength) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { int1 = signalStrength.level },
+            { "onCarrierRoamingNtnSignalStrengthChanged: level=$int1" },
         )
     }
 
@@ -128,7 +134,7 @@
             TAG,
             LogLevel.INFO,
             { bool1 = active },
-            { "onCarrierRoamingNtnModeChanged: $bool1" }
+            { "onCarrierRoamingNtnModeChanged: $bool1" },
         )
     }
 
@@ -146,12 +152,7 @@
     }
 
     fun logCarrierConfigChanged(subId: Int) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            { int1 = subId },
-            { "onCarrierConfigChanged: subId=$int1" },
-        )
+        buffer.log(TAG, LogLevel.INFO, { int1 = subId }, { "onCarrierConfigChanged: subId=$int1" })
     }
 
     fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) {
@@ -175,7 +176,7 @@
             TAG,
             LogLevel.INFO,
             { str1 = config.toString() },
-            { "defaultDataSubRatConfig: $str1" }
+            { "defaultDataSubRatConfig: $str1" },
         )
     }
 
@@ -184,7 +185,7 @@
             TAG,
             LogLevel.INFO,
             { str1 = mapping.toString() },
-            { "defaultMobileIconMapping: $str1" }
+            { "defaultMobileIconMapping: $str1" },
         )
     }
 
@@ -216,7 +217,7 @@
             {
                 "Intent: ACTION_SERVICE_PROVIDERS_UPDATED." +
                     " showSpn=$bool1 spn=$str1 dataSpn=$str2 showPlmn=$bool2 plmn=$str3"
-            }
+            },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 205205e..07843f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -107,6 +107,12 @@
     // @IntRange(from = 0, to = 4)
     val primaryLevel: StateFlow<Int>
 
+    /**
+     * This level can be used to reflect the signal strength when in carrier roaming NTN mode
+     * (carrier-based satellite)
+     */
+    val satelliteLevel: StateFlow<Int>
+
     /** The current data connection state. See [DataConnectionState] */
     val dataConnectionState: StateFlow<DataConnectionState>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index 3261b71..be3977e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -37,12 +37,14 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_SATELLITE_LEVEL
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -75,7 +77,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = "inflate",
-                _inflateSignalStrength.value
+                _inflateSignalStrength.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value)
 
@@ -89,7 +91,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_EMERGENCY,
-                _isEmergencyOnly.value
+                _isEmergencyOnly.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value)
 
@@ -100,7 +102,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_ROAMING,
-                _isRoaming.value
+                _isRoaming.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value)
 
@@ -111,7 +113,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_OPERATOR,
-                _operatorAlphaShort.value
+                _operatorAlphaShort.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value)
 
@@ -122,7 +124,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_IN_SERVICE,
-                _isInService.value
+                _isInService.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value)
 
@@ -133,7 +135,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_NTN,
-                _isNonTerrestrial.value
+                _isNonTerrestrial.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value)
 
@@ -144,7 +146,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_GSM,
-                _isGsm.value
+                _isGsm.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value)
 
@@ -155,7 +157,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CDMA_LEVEL,
-                _cdmaLevel.value
+                _cdmaLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value)
 
@@ -166,10 +168,21 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_PRIMARY_LEVEL,
-                _primaryLevel.value
+                _primaryLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value)
 
+    private val _satelliteLevel = MutableStateFlow(0)
+    override val satelliteLevel: StateFlow<Int> =
+        _satelliteLevel
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_SATELLITE_LEVEL,
+                _satelliteLevel.value,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), _satelliteLevel.value)
+
     private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected)
     override val dataConnectionState =
         _dataConnectionState
@@ -177,12 +190,7 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value)
 
     private val _dataActivityDirection =
-        MutableStateFlow(
-            DataActivityModel(
-                hasActivityIn = false,
-                hasActivityOut = false,
-            )
-        )
+        MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
     override val dataActivityDirection =
         _dataActivityDirection
             .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value)
@@ -195,7 +203,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CARRIER_NETWORK_CHANGE,
-                _carrierNetworkChangeActive.value
+                _carrierNetworkChangeActive.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index 2e47678..75f613d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -90,7 +90,7 @@
                         TAG,
                         "Connection repo subId=$subId " +
                             "does not equal wifi repo subId=${network.subscriptionId}; " +
-                            "not showing carrier merged"
+                            "not showing carrier merged",
                     )
                     null
                 }
@@ -149,7 +149,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                ResolvedNetworkType.UnknownNetworkType
+                ResolvedNetworkType.UnknownNetworkType,
             )
 
     override val dataConnectionState =
@@ -173,6 +173,7 @@
     override val isNonTerrestrial = MutableStateFlow(false).asStateFlow()
     override val isGsm = MutableStateFlow(false).asStateFlow()
     override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow()
+    override val satelliteLevel = MutableStateFlow(0)
 
     /**
      * Carrier merged connections happen over wifi but are displayed as a mobile triangle. Because
@@ -207,10 +208,7 @@
         @Application private val scope: CoroutineScope,
         private val wifiRepository: WifiRepository,
     ) {
-        fun build(
-            subId: Int,
-            mobileLogger: TableLogBuffer,
-        ): MobileConnectionRepository {
+        fun build(subId: Int, mobileLogger: TableLogBuffer): MobileConnectionRepository {
             return CarrierMergedConnectionRepository(
                 subId,
                 mobileLogger,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index a5e47a6..fae9be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -132,12 +132,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_EMERGENCY,
-                activeRepo.value.isEmergencyOnly.value
+                activeRepo.value.isEmergencyOnly.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.isEmergencyOnly.value
+                activeRepo.value.isEmergencyOnly.value,
             )
 
     override val isRoaming =
@@ -147,7 +147,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_ROAMING,
-                activeRepo.value.isRoaming.value
+                activeRepo.value.isRoaming.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value)
 
@@ -158,12 +158,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_OPERATOR,
-                activeRepo.value.operatorAlphaShort.value
+                activeRepo.value.operatorAlphaShort.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.operatorAlphaShort.value
+                activeRepo.value.operatorAlphaShort.value,
             )
 
     override val isInService =
@@ -173,7 +173,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_IN_SERVICE,
-                activeRepo.value.isInService.value
+                activeRepo.value.isInService.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value)
 
@@ -184,12 +184,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_NTN,
-                activeRepo.value.isNonTerrestrial.value
+                activeRepo.value.isNonTerrestrial.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.isNonTerrestrial.value
+                activeRepo.value.isNonTerrestrial.value,
             )
 
     override val isGsm =
@@ -199,7 +199,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_GSM,
-                activeRepo.value.isGsm.value
+                activeRepo.value.isGsm.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value)
 
@@ -210,7 +210,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CDMA_LEVEL,
-                activeRepo.value.cdmaLevel.value
+                activeRepo.value.cdmaLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value)
 
@@ -221,22 +221,33 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_PRIMARY_LEVEL,
-                activeRepo.value.primaryLevel.value
+                activeRepo.value.primaryLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value)
 
+    override val satelliteLevel: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.satelliteLevel }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_SATELLITE_LEVEL,
+                activeRepo.value.satelliteLevel.value,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value)
+
     override val dataConnectionState =
         activeRepo
             .flatMapLatest { it.dataConnectionState }
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.dataConnectionState.value
+                activeRepo.value.dataConnectionState.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.dataConnectionState.value
+                activeRepo.value.dataConnectionState.value,
             )
 
     override val dataActivityDirection =
@@ -245,12 +256,12 @@
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.dataActivityDirection.value
+                activeRepo.value.dataActivityDirection.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.dataActivityDirection.value
+                activeRepo.value.dataActivityDirection.value,
             )
 
     override val carrierNetworkChangeActive =
@@ -260,12 +271,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CARRIER_NETWORK_CHANGE,
-                activeRepo.value.carrierNetworkChangeActive.value
+                activeRepo.value.carrierNetworkChangeActive.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.carrierNetworkChangeActive.value
+                activeRepo.value.carrierNetworkChangeActive.value,
             )
 
     override val resolvedNetworkType =
@@ -274,12 +285,12 @@
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.resolvedNetworkType.value
+                activeRepo.value.resolvedNetworkType.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.resolvedNetworkType.value
+                activeRepo.value.resolvedNetworkType.value,
             )
 
     override val dataEnabled =
@@ -305,7 +316,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.inflateSignalStrength.value
+                activeRepo.value.inflateSignalStrength.value,
             )
 
     override val allowNetworkSliceIndicator =
@@ -320,7 +331,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.allowNetworkSliceIndicator.value
+                activeRepo.value.allowNetworkSliceIndicator.value,
             )
 
     override val numberOfLevels =
@@ -439,6 +450,7 @@
         const val COL_IS_IN_SERVICE = "isInService"
         const val COL_OPERATOR = "operatorName"
         const val COL_PRIMARY_LEVEL = "primaryLevel"
+        const val COL_SATELLITE_LEVEL = "satelliteLevel"
         const val COL_ROAMING = "roaming"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 62bd8ad..8a1e7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -41,6 +41,7 @@
 import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
+import android.telephony.satellite.NtnSignalStrength
 import com.android.settingslib.Utils
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -173,7 +174,7 @@
 
                         override fun onDataConnectionStateChanged(
                             dataState: Int,
-                            networkType: Int
+                            networkType: Int,
                         ) {
                             logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
                             trySend(CallbackEvent.OnDataConnectionStateChanged(dataState))
@@ -195,6 +196,17 @@
                             logger.logOnSignalStrengthsChanged(signalStrength, subId)
                             trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength))
                         }
+
+                        override fun onCarrierRoamingNtnSignalStrengthChanged(
+                            signalStrength: NtnSignalStrength
+                        ) {
+                            logger.logNtnSignalStrengthChanged(signalStrength)
+                            trySend(
+                                CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged(
+                                    signalStrength
+                                )
+                            )
+                        }
                     }
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
@@ -267,6 +279,12 @@
             .map { it.signalStrength.level }
             .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
 
+    override val satelliteLevel: StateFlow<Int> =
+        callbackEvents
+            .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged }
+            .map { it.signalStrength.level }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+
     override val dataConnectionState =
         callbackEvents
             .mapNotNull { it.onDataConnectionStateChanged }
@@ -280,7 +298,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false),
             )
 
     override val carrierNetworkChangeActive =
@@ -385,7 +403,7 @@
                             if (
                                 intent.getIntExtra(
                                     EXTRA_SUBSCRIPTION_INDEX,
-                                    INVALID_SUBSCRIPTION_ID
+                                    INVALID_SUBSCRIPTION_ID,
                                 ) == subId
                             ) {
                                 logger.logServiceProvidersUpdatedBroadcast(intent)
@@ -399,7 +417,7 @@
 
                 context.registerReceiver(
                     receiver,
-                    IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)
+                    IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED),
                 )
 
                 awaitClose { context.unregisterReceiver(receiver) }
@@ -524,6 +542,9 @@
     data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent
 
     data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent
+
+    data class OnCarrierRoamingNtnSignalStrengthChanged(val signalStrength: NtnSignalStrength) :
+        CallbackEvent
 }
 
 /**
@@ -539,6 +560,9 @@
     val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null,
     val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null,
     val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null,
+    val onCarrierRoamingNtnSignalStrengthChanged:
+        CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged? =
+        null,
 ) {
     fun applyEvent(event: CallbackEvent): TelephonyCallbackState {
         return when (event) {
@@ -555,6 +579,8 @@
                 copy(onServiceStateChanged = event)
             }
             is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event)
+            is CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged ->
+                copy(onCarrierRoamingNtnSignalStrengthChanged = event)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 4ef328c..1bf14af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -335,7 +335,11 @@
     // Satellite level is unaffected by the inflateSignalStrength property
     // See b/346904529 for details
     private val satelliteShownLevel: StateFlow<Int> =
-        combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+        if (Flags.carrierRoamingNbIotNtn()) {
+                connectionRepository.satelliteLevel
+            } else {
+                combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     private val cellularIcon: Flow<SignalIconModel.Cellular> =
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 11d7339..2177e02 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
@@ -22,6 +22,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -30,13 +31,12 @@
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
 import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment
+import com.android.systemui.statusbar.core.StatusBarRootModernization
 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
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel
 import javax.inject.Inject
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Interface to assist with binding the [CollapsedStatusBarFragment] to [HomeStatusBarViewModel].
@@ -91,7 +91,7 @@
                     launch {
                         viewModel.primaryOngoingActivityChip.collect { primaryChipModel ->
                             OngoingActivityChipBinder.bind(primaryChipModel, primaryChipView)
-                            if (StatusBarSimpleFragment.isEnabled) {
+                            if (StatusBarRootModernization.isEnabled) {
                                 when (primaryChipModel) {
                                     is OngoingActivityChipModel.Shown ->
                                         primaryChipView.show(shouldAnimateChange = true)
@@ -133,7 +133,7 @@
                             // enough space for it.
                             OngoingActivityChipBinder.bind(chips.secondary, secondaryChipView)
 
-                            if (StatusBarSimpleFragment.isEnabled) {
+                            if (StatusBarRootModernization.isEnabled) {
                                 primaryChipView.adjustVisibility(chips.primary.toVisibilityModel())
                                 secondaryChipView.adjustVisibility(
                                     chips.secondary.toVisibilityModel()
@@ -160,7 +160,7 @@
                     }
                 }
 
-                if (StatusBarSimpleFragment.isEnabled) {
+                if (StatusBarRootModernization.isEnabled) {
                     val clockView = view.requireViewById<View>(R.id.clock)
                     launch { viewModel.isClockVisible.collect { clockView.adjustVisibility(it) } }
 
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 247abc3..a9c2f72 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
@@ -104,7 +104,7 @@
     darkIconDispatcher: DarkIconDispatcher,
     onViewCreated: (ViewGroup) -> Unit,
 ) {
-    // None of these methods are used when [StatusBarSimpleFragment] is on.
+    // None of these methods are used when [StatusBarRootModernization] is on.
     // This can be deleted once the fragment is gone
     val nopVisibilityChangeListener =
         object : StatusBarVisibilityChangeListener {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index 76024cd..c7b6be3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -17,12 +17,15 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
 
 import android.annotation.SuppressLint
+import android.content.Context
 import android.net.wifi.ScanResult
 import android.net.wifi.WifiManager
+import android.os.UserHandle
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -43,6 +46,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Unavailable.toHotspotDeviceType
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.user.data.repository.UserRepository
 import com.android.wifitrackerlib.HotspotNetworkEntry
 import com.android.wifitrackerlib.MergedCarrierEntry
 import com.android.wifitrackerlib.WifiEntry
@@ -53,10 +57,12 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -68,6 +74,8 @@
 class WifiRepositoryImpl
 @Inject
 constructor(
+    @Application applicationContext: Context,
+    private val userRepository: UserRepository,
     @Application private val scope: CoroutineScope,
     @Main private val mainExecutor: Executor,
     @Background private val bgDispatcher: CoroutineDispatcher,
@@ -84,90 +92,226 @@
 
     private var wifiPickerTracker: WifiPickerTracker? = null
 
-    private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> = run {
-        var current =
-            WifiPickerTrackerInfo(
-                state = WIFI_STATE_DEFAULT,
-                isDefault = false,
-                primaryNetwork = WIFI_NETWORK_DEFAULT,
-                secondaryNetworks = emptyList(),
-            )
-        callbackFlow {
-                val callback =
-                    object : WifiPickerTracker.WifiPickerTrackerCallback {
-                        override fun onWifiEntriesChanged() {
-                            val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection
-                            logOnWifiEntriesChanged(connectedEntry)
+    @VisibleForTesting
+    val selectedUserContext: Flow<Context> =
+        userRepository.selectedUserInfo.map {
+            applicationContext.createContextAsUser(UserHandle.of(it.id), /* flags= */ 0)
+        }
 
-                            val activeNetworks = wifiPickerTracker?.activeWifiEntries ?: emptyList()
-                            val secondaryNetworks =
-                                activeNetworks
-                                    .filter { it != connectedEntry && !it.isPrimaryNetwork }
-                                    .map { it.toWifiNetworkModel() }
+    var current =
+        WifiPickerTrackerInfo(
+            state = WIFI_STATE_DEFAULT,
+            isDefault = false,
+            primaryNetwork = WIFI_NETWORK_DEFAULT,
+            secondaryNetworks = emptyList(),
+        )
 
-                            // [WifiPickerTracker.connectedWifiEntry] will return the same instance
-                            // but with updated internals. For example, when its validation status
-                            // changes from false to true, the same instance is re-used but with the
-                            // validated field updated.
-                            //
-                            // Because it's the same instance, the flow won't re-emit the value
-                            // (even though the internals have changed). So, we need to transform it
-                            // into our internal model immediately. [toWifiNetworkModel] always
-                            // returns a new instance, so the flow is guaranteed to emit.
-                            send(
-                                newPrimaryNetwork =
-                                    connectedEntry?.toPrimaryWifiNetworkModel()
-                                        ?: WIFI_NETWORK_DEFAULT,
-                                newSecondaryNetworks = secondaryNetworks,
-                                newIsDefault = connectedEntry?.isDefaultNetwork ?: false,
-                            )
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    private val wifiPickerTrackerInfo: StateFlow<WifiPickerTrackerInfo> =
+        if (multiuserWifiPickerTrackerSupport()) {
+            selectedUserContext
+                .flatMapLatest { currentContext
+                    -> // flatMapLatest because when selectedUserContext emits a new value, we want
+                    // to
+                    // re-create a whole flow
+                    callbackFlow {
+                            val callback =
+                                object : WifiPickerTracker.WifiPickerTrackerCallback {
+                                    override fun onWifiEntriesChanged() {
+                                        val connectedEntry =
+                                            wifiPickerTracker.mergedOrPrimaryConnection
+                                        logOnWifiEntriesChanged(connectedEntry)
+
+                                        val activeNetworks =
+                                            wifiPickerTracker?.activeWifiEntries ?: emptyList()
+                                        val secondaryNetworks =
+                                            activeNetworks
+                                                .filter {
+                                                    it != connectedEntry && !it.isPrimaryNetwork
+                                                }
+                                                .map { it.toWifiNetworkModel() }
+
+                                        // [WifiPickerTracker.connectedWifiEntry] will return the
+                                        // same
+                                        // instance but with updated internals. For example, when
+                                        // its
+                                        // validation status changes from false to true, the same
+                                        // instance is re-used but with the validated field updated.
+                                        //
+                                        // Because it's the same instance, the flow won't re-emit
+                                        // the
+                                        // value (even though the internals have changed). So, we
+                                        // need
+                                        // to transform it into our internal model immediately.
+                                        // [toWifiNetworkModel] always returns a new instance, so
+                                        // the
+                                        // flow is guaranteed to emit.
+                                        send(
+                                            newPrimaryNetwork =
+                                                connectedEntry?.toPrimaryWifiNetworkModel()
+                                                    ?: WIFI_NETWORK_DEFAULT,
+                                            newSecondaryNetworks = secondaryNetworks,
+                                            newIsDefault = connectedEntry?.isDefaultNetwork ?: false,
+                                        )
+                                    }
+
+                                    override fun onWifiStateChanged() {
+                                        val state = wifiPickerTracker?.wifiState
+                                        logOnWifiStateChanged(state)
+                                        send(newState = state ?: WIFI_STATE_DEFAULT)
+                                    }
+
+                                    override fun onNumSavedNetworksChanged() {}
+
+                                    override fun onNumSavedSubscriptionsChanged() {}
+
+                                    private fun send(
+                                        newState: Int = current.state,
+                                        newIsDefault: Boolean = current.isDefault,
+                                        newPrimaryNetwork: WifiNetworkModel =
+                                            current.primaryNetwork,
+                                        newSecondaryNetworks: List<WifiNetworkModel> =
+                                            current.secondaryNetworks,
+                                    ) {
+                                        val new =
+                                            WifiPickerTrackerInfo(
+                                                newState,
+                                                newIsDefault,
+                                                newPrimaryNetwork,
+                                                newSecondaryNetworks,
+                                            )
+                                        current = new
+                                        trySend(new)
+                                    }
+                                }
+                            wifiPickerTracker =
+                                wifiPickerTrackerFactory
+                                    .create(currentContext, lifecycle, callback, "WifiRepository")
+                                    .apply {
+                                        // By default, [WifiPickerTracker] will scan to see all
+                                        // available wifi networks in the area. Because SysUI only
+                                        // needs to display the **connected** network, we don't
+                                        // need scans to be running (and in fact, running scans is
+                                        // costly and should be avoided whenever possible).
+                                        this?.disableScanning()
+                                    }
+
+                            // The lifecycle must be STARTED in order for the callback to receive
+                            // events.
+                            mainExecutor.execute {
+                                lifecycle.currentState = Lifecycle.State.STARTED
+                            }
+                            awaitClose {
+                                mainExecutor.execute {
+                                    lifecycle.currentState = Lifecycle.State.CREATED
+                                }
+                            }
                         }
-
-                        override fun onWifiStateChanged() {
-                            val state = wifiPickerTracker?.wifiState
-                            logOnWifiStateChanged(state)
-                            send(newState = state ?: WIFI_STATE_DEFAULT)
-                        }
-
-                        override fun onNumSavedNetworksChanged() {}
-
-                        override fun onNumSavedSubscriptionsChanged() {}
-
-                        private fun send(
-                            newState: Int = current.state,
-                            newIsDefault: Boolean = current.isDefault,
-                            newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork,
-                            newSecondaryNetworks: List<WifiNetworkModel> =
-                                current.secondaryNetworks,
-                        ) {
-                            val new =
-                                WifiPickerTrackerInfo(
-                                    newState,
-                                    newIsDefault,
-                                    newPrimaryNetwork,
-                                    newSecondaryNetworks,
-                                )
-                            current = new
-                            trySend(new)
-                        }
-                    }
-
-                wifiPickerTracker =
-                    wifiPickerTrackerFactory.create(lifecycle, callback, "WifiRepository").apply {
-                        // By default, [WifiPickerTracker] will scan to see all available wifi
-                        // networks in the area. Because SysUI only needs to display the
-                        // **connected** network, we don't need scans to be running (and in fact,
-                        // running scans is costly and should be avoided whenever possible).
-                        this?.disableScanning()
-                    }
-                // The lifecycle must be STARTED in order for the callback to receive events.
-                mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED }
-                awaitClose {
-                    mainExecutor.execute { lifecycle.currentState = Lifecycle.State.CREATED }
+                        .stateIn(scope, SharingStarted.Eagerly, current)
                 }
+                .stateIn(scope, SharingStarted.Eagerly, current)
+        } else {
+
+            run {
+                var current =
+                    WifiPickerTrackerInfo(
+                        state = WIFI_STATE_DEFAULT,
+                        isDefault = false,
+                        primaryNetwork = WIFI_NETWORK_DEFAULT,
+                        secondaryNetworks = emptyList(),
+                    )
+                callbackFlow {
+                        val callback =
+                            object : WifiPickerTracker.WifiPickerTrackerCallback {
+                                override fun onWifiEntriesChanged() {
+                                    val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection
+                                    logOnWifiEntriesChanged(connectedEntry)
+
+                                    val activeNetworks =
+                                        wifiPickerTracker?.activeWifiEntries ?: emptyList()
+                                    val secondaryNetworks =
+                                        activeNetworks
+                                            .filter { it != connectedEntry && !it.isPrimaryNetwork }
+                                            .map { it.toWifiNetworkModel() }
+
+                                    // [WifiPickerTracker.connectedWifiEntry] will return the same
+                                    // instance
+                                    // but with updated internals. For example, when its validation
+                                    // status
+                                    // changes from false to true, the same instance is re-used but
+                                    // with the
+                                    // validated field updated.
+                                    //
+                                    // Because it's the same instance, the flow won't re-emit the
+                                    // value
+                                    // (even though the internals have changed). So, we need to
+                                    // transform it
+                                    // into our internal model immediately. [toWifiNetworkModel]
+                                    // always
+                                    // returns a new instance, so the flow is guaranteed to emit.
+                                    send(
+                                        newPrimaryNetwork =
+                                            connectedEntry?.toPrimaryWifiNetworkModel()
+                                                ?: WIFI_NETWORK_DEFAULT,
+                                        newSecondaryNetworks = secondaryNetworks,
+                                        newIsDefault = connectedEntry?.isDefaultNetwork ?: false,
+                                    )
+                                }
+
+                                override fun onWifiStateChanged() {
+                                    val state = wifiPickerTracker?.wifiState
+                                    logOnWifiStateChanged(state)
+                                    send(newState = state ?: WIFI_STATE_DEFAULT)
+                                }
+
+                                override fun onNumSavedNetworksChanged() {}
+
+                                override fun onNumSavedSubscriptionsChanged() {}
+
+                                private fun send(
+                                    newState: Int = current.state,
+                                    newIsDefault: Boolean = current.isDefault,
+                                    newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork,
+                                    newSecondaryNetworks: List<WifiNetworkModel> =
+                                        current.secondaryNetworks,
+                                ) {
+                                    val new =
+                                        WifiPickerTrackerInfo(
+                                            newState,
+                                            newIsDefault,
+                                            newPrimaryNetwork,
+                                            newSecondaryNetworks,
+                                        )
+                                    current = new
+                                    trySend(new)
+                                }
+                            }
+
+                        wifiPickerTracker =
+                            wifiPickerTrackerFactory
+                                .create(applicationContext, lifecycle, callback, "WifiRepository")
+                                .apply {
+                                    // By default, [WifiPickerTracker] will scan to see all
+                                    // available wifi
+                                    // networks in the area. Because SysUI only needs to display the
+                                    // **connected** network, we don't need scans to be running (and
+                                    // in fact,
+                                    // running scans is costly and should be avoided whenever
+                                    // possible).
+                                    this?.disableScanning()
+                                }
+                        // The lifecycle must be STARTED in order for the callback to receive
+                        // events.
+                        mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED }
+                        awaitClose {
+                            mainExecutor.execute {
+                                lifecycle.currentState = Lifecycle.State.CREATED
+                            }
+                        }
+                    }
+                    .stateIn(scope, SharingStarted.Eagerly, current)
             }
-            .stateIn(scope, SharingStarted.Eagerly, current)
-    }
+        }
 
     override val isWifiEnabled: StateFlow<Boolean> =
         wifiPickerTrackerInfo
@@ -185,11 +329,7 @@
         wifiPickerTrackerInfo
             .map { it.primaryNetwork }
             .distinctUntilChanged()
-            .logDiffsForTable(
-                tableLogger,
-                columnPrefix = "",
-                initialValue = WIFI_NETWORK_DEFAULT,
-            )
+            .logDiffsForTable(tableLogger, columnPrefix = "", initialValue = WIFI_NETWORK_DEFAULT)
             .stateIn(scope, SharingStarted.Eagerly, WIFI_NETWORK_DEFAULT)
 
     override val secondaryNetworks: StateFlow<List<WifiNetworkModel>> =
@@ -348,7 +488,7 @@
             TAG,
             LogLevel.DEBUG,
             { str1 = prettyPrintActivity(activity) },
-            { "onActivityChanged: $str1" }
+            { "onActivityChanged: $str1" },
         )
     }
 
@@ -379,13 +519,15 @@
         /** The currently primary wifi network. */
         val primaryNetwork: WifiNetworkModel,
         /** The current secondary network(s), if any. Specifically excludes the primary network. */
-        val secondaryNetworks: List<WifiNetworkModel>
+        val secondaryNetworks: List<WifiNetworkModel>,
     )
 
     @SysUISingleton
     class Factory
     @Inject
     constructor(
+        @Application private val applicationContext: Context,
+        private val userRepository: UserRepository,
         @Application private val scope: CoroutineScope,
         @Main private val mainExecutor: Executor,
         @Background private val bgDispatcher: CoroutineDispatcher,
@@ -395,6 +537,8 @@
     ) {
         fun create(wifiManager: WifiManager): WifiRepositoryImpl {
             return WifiRepositoryImpl(
+                applicationContext,
+                userRepository,
                 scope,
                 mainExecutor,
                 bgDispatcher,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index c7bd5a1..9187e3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -206,12 +206,14 @@
     @SysUISingleton
     @Provides
     static AccessPointControllerImpl  provideAccessPointControllerImpl(
+            @Application Context context,
             UserManager userManager,
             UserTracker userTracker,
             @Main Executor mainExecutor,
             WifiPickerTrackerFactory wifiPickerTrackerFactory
     ) {
         AccessPointControllerImpl controller = new AccessPointControllerImpl(
+                context,
                 userManager,
                 userTracker,
                 mainExecutor,
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 3f6ef16..b79d39e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowView.java
@@ -35,7 +35,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.compose.ComposeInitializer;
-import com.android.systemui.statusbar.core.StatusBarSimpleFragment;
+import com.android.systemui.statusbar.core.StatusBarRootModernization;
 import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
 
 /**
@@ -64,7 +64,7 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
-        if (StatusBarSimpleFragment.isEnabled()) {
+        if (StatusBarRootModernization.isEnabled()) {
             ComposeInitializer.INSTANCE.onAttachedToWindow(this);
         }
     }
@@ -73,7 +73,7 @@
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
 
-        if (StatusBarSimpleFragment.isEnabled()) {
+        if (StatusBarRootModernization.isEnabled()) {
             ComposeInitializer.INSTANCE.onDetachedFromWindow(this);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index f20ce63..ad97b21 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -23,11 +23,13 @@
 import android.content.Context
 import android.content.IntentFilter
 import android.content.pm.UserInfo
+import android.content.res.Resources
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
 import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.statusbar.IStatusBarService
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -109,6 +111,9 @@
     /** Whether logout for secondary users is enabled by admin device policy. */
     val isSecondaryUserLogoutEnabled: StateFlow<Boolean>
 
+    /** Whether logout into system user is enabled. */
+    val isLogoutToSystemUserEnabled: StateFlow<Boolean>
+
     /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
     fun refreshUsers()
 
@@ -121,6 +126,9 @@
     /** Performs logout logout for secondary users. */
     suspend fun logOutSecondaryUser()
 
+    /** Performs logout into the system user. */
+    suspend fun logOutToSystemUser()
+
     /**
      * Returns the user ID of the "main user" of the device. This user may have access to certain
      * features which are limited to at most one user. There will never be more than one main user
@@ -143,6 +151,7 @@
 @Inject
 constructor(
     @Application private val appContext: Context,
+    @Main private val resources: Resources,
     private val manager: UserManager,
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
@@ -151,6 +160,7 @@
     private val tracker: UserTracker,
     private val devicePolicyManager: DevicePolicyManager,
     private val broadcastDispatcher: BroadcastDispatcher,
+    private val statusBarService: IStatusBarService,
 ) : UserRepository {
 
     private val _userSwitcherSettings: StateFlow<UserSwitcherSettingsModel> =
@@ -275,12 +285,34 @@
             .stateIn(applicationScope, SharingStarted.Eagerly, false)
 
     @SuppressLint("MissingPermission")
+    override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
+        selectedUser
+            .flatMapLatestConflated { selectedUser ->
+                if (selectedUser.isEligibleForLogout()) {
+                    flowOf(
+                        resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen)
+                    )
+                } else {
+                    flowOf(false)
+                }
+            }
+            .stateIn(applicationScope, SharingStarted.Eagerly, false)
+
+    @SuppressLint("MissingPermission")
     override suspend fun logOutSecondaryUser() {
         if (isSecondaryUserLogoutEnabled.value) {
             withContext(backgroundDispatcher) { devicePolicyManager.logoutUser() }
         }
     }
 
+    override suspend fun logOutToSystemUser() {
+        // TODO(b/377493351) : start using proper logout API once it is available.
+        // Using reboot is a temporary solution.
+        if (isLogoutToSystemUserEnabled.value) {
+            withContext(backgroundDispatcher) { statusBarService.reboot(false) }
+        }
+    }
+
     @SuppressLint("MissingPermission")
     override fun refreshUsers() {
         applicationScope.launch {
@@ -316,42 +348,53 @@
 
     private suspend fun getSettings(): UserSwitcherSettingsModel {
         return withContext(backgroundDispatcher) {
-            val isSimpleUserSwitcher =
-                globalSettings.getInt(
-                    SETTING_SIMPLE_USER_SWITCHER,
-                    if (
-                        appContext.resources.getBoolean(
-                            com.android.internal.R.bool.config_expandLockScreenUserSwitcher
-                        )
-                    ) {
-                        1
-                    } else {
-                        0
-                    },
-                ) != 0
+            if (
+                // TODO(b/378068979): remove once login screen-specific logic
+                // is implemented at framework level.
+                appContext.resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen)
+            ) {
+                UserSwitcherSettingsModel(
+                    isSimpleUserSwitcher = false,
+                    isAddUsersFromLockscreen = false,
+                    isUserSwitcherEnabled = false,
+                )
+            } else {
+                val isSimpleUserSwitcher =
+                    globalSettings.getInt(
+                        SETTING_SIMPLE_USER_SWITCHER,
+                        if (
+                            appContext.resources.getBoolean(
+                                com.android.internal.R.bool.config_expandLockScreenUserSwitcher
+                            )
+                        ) {
+                            1
+                        } else {
+                            0
+                        },
+                    ) != 0
 
-            val isAddUsersFromLockscreen =
-                globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0
+                val isAddUsersFromLockscreen =
+                    globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0
 
-            val isUserSwitcherEnabled =
-                globalSettings.getInt(
-                    Settings.Global.USER_SWITCHER_ENABLED,
-                    if (
-                        appContext.resources.getBoolean(
-                            com.android.internal.R.bool.config_showUserSwitcherByDefault
-                        )
-                    ) {
-                        1
-                    } else {
-                        0
-                    },
-                ) != 0
-
-            UserSwitcherSettingsModel(
-                isSimpleUserSwitcher = isSimpleUserSwitcher,
-                isAddUsersFromLockscreen = isAddUsersFromLockscreen,
-                isUserSwitcherEnabled = isUserSwitcherEnabled,
-            )
+                val isUserSwitcherEnabled =
+                    globalSettings.getInt(
+                        Settings.Global.USER_SWITCHER_ENABLED,
+                        if (
+                            appContext.resources.getBoolean(
+                                com.android.internal.R.bool.config_showUserSwitcherByDefault
+                            )
+                        ) {
+                            1
+                        } else {
+                            0
+                        },
+                    ) != 0
+                UserSwitcherSettingsModel(
+                    isSimpleUserSwitcher = isSimpleUserSwitcher,
+                    isAddUsersFromLockscreen = isAddUsersFromLockscreen,
+                    isUserSwitcherEnabled = isUserSwitcherEnabled,
+                )
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
index a798360..bcbd679 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
@@ -21,6 +21,7 @@
 import android.os.Handler
 import android.os.UserManager
 import android.provider.Settings.Global.USER_SWITCHER_ENABLED
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -40,7 +41,6 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.withContext
 
 interface UserSwitcherRepository {
@@ -67,6 +67,9 @@
     private val showUserSwitcherForSingleUser =
         context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)
 
+    private val userSwitchingMustGoThroughLoginScreen =
+        context.resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen)
+
     override val isEnabled: Flow<Boolean> = conflatedCallbackFlow {
         suspend fun updateState() {
             trySendWithFailureLogging(isUserSwitcherEnabled(), TAG)
@@ -135,7 +138,13 @@
 
     private suspend fun isUserSwitcherEnabled(): Boolean {
         return withContext(bgDispatcher) {
-            userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser)
+            // TODO(b/378068979): remove once login screen-specific logic
+            // is implemented at framework level.
+            if (userSwitchingMustGoThroughLoginScreen) {
+                false
+            } else {
+                userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser)
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt
index 154f1dc..f2dd25f 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt
@@ -23,7 +23,10 @@
 import com.android.systemui.user.data.repository.UserRepository
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates business logic to for the logout. */
 @SysUISingleton
@@ -33,11 +36,22 @@
     private val userRepository: UserRepository,
     @Application private val applicationScope: CoroutineScope,
 ) {
-    val isLogoutEnabled: StateFlow<Boolean> = userRepository.isSecondaryUserLogoutEnabled
+
+    val isLogoutEnabled: StateFlow<Boolean> =
+        combine(
+                userRepository.isSecondaryUserLogoutEnabled,
+                userRepository.isLogoutToSystemUserEnabled,
+                Boolean::or,
+            )
+            .stateIn(applicationScope, SharingStarted.Eagerly, false)
 
     fun logOut() {
-        if (userRepository.isSecondaryUserLogoutEnabled.value) {
-            applicationScope.launch { userRepository.logOutSecondaryUser() }
+        applicationScope.launch {
+            if (userRepository.isSecondaryUserLogoutEnabled.value) {
+                userRepository.logOutSecondaryUser()
+            } else if (userRepository.isLogoutToSystemUserEnabled.value) {
+                userRepository.logOutToSystemUser()
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt
index 71335ec..bc3726d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt
@@ -30,6 +30,7 @@
  * Repository for observing values of [Settings.Secure] for the currently active user. That means
  * when user is switched and the new user has different value, flow will emit new value.
  */
+// TODO: b/377244768 - Make internal once call sites inject SecureSettingsRepository instead.
 @SysUISingleton
 class UserAwareSecureSettingsRepository
 @Inject
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
index a31b8d9..49a0f14 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.util.settings.repository
 
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -34,10 +33,10 @@
 
 /**
  * Repository for observing values of a [UserSettingsProxy], for the currently active user. That
- * means that when user is switched and the new user has a different value, the flow will emit the
- * new value.
+ * means that when the user is switched and the new user has a different value, the flow will emit
+ * the new value.
  */
-@SysUISingleton
+// TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal.
 @OptIn(ExperimentalCoroutinesApi::class)
 abstract class UserAwareSettingsRepository(
     private val userSettings: UserSettingsProxy,
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
index 8b1fca5..4b01ded 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
@@ -17,12 +17,10 @@
 package com.android.systemui.util.settings.repository
 
 import android.provider.Settings
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.settings.SystemSettings
-import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
 
@@ -30,10 +28,8 @@
  * Repository for observing values of [Settings.Secure] for the currently active user. That means
  * when user is switched and the new user has different value, flow will emit new value.
  */
-@SysUISingleton
-class UserAwareSystemSettingsRepository
-@Inject
-constructor(
+// TODO: b/377244768 - Make internal once call sites inject SystemSettingsRepository instead.
+class UserAwareSystemSettingsRepository(
     systemSettings: SystemSettings,
     userRepository: UserRepository,
     @Background backgroundDispatcher: CoroutineDispatcher,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
index 9440a93..fb15795 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.dialog.dagger
 
+import com.android.systemui.volume.dialog.dagger.module.VolumeDialogModule
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
 import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent
@@ -28,7 +29,7 @@
  * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it.
  */
 @VolumeDialogScope
-@Subcomponent(modules = [])
+@Subcomponent(modules = [VolumeDialogModule::class])
 interface VolumeDialogComponent {
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt
new file mode 100644
index 0000000..025e269
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.volume.dialog.dagger.module
+
+import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository
+import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/** Dagger module for volume dialog code in the volume package */
+@Module
+interface VolumeDialogModule {
+
+    @Binds
+    fun bindVolumeDialogRingerFeedbackRepository(
+        ringerFeedbackRepository: VolumeDialogRingerFeedbackRepositoryImpl
+    ): VolumeDialogRingerFeedbackRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.kt
new file mode 100644
index 0000000..263972b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepository.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.volume.dialog.ringer.data.repository
+
+import android.content.Context
+import com.android.systemui.Prefs
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+interface VolumeDialogRingerFeedbackRepository {
+
+    /** gets number of shown toasts */
+    suspend fun getToastCount(): Int
+
+    /** updates number of shown toasts */
+    suspend fun updateToastCount(toastCount: Int)
+}
+
+class VolumeDialogRingerFeedbackRepositoryImpl
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    @Background val backgroundDispatcher: CoroutineDispatcher,
+) : VolumeDialogRingerFeedbackRepository {
+
+    override suspend fun getToastCount(): Int =
+        withContext(backgroundDispatcher) {
+            return@withContext Prefs.getInt(
+                applicationContext,
+                Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT,
+                0,
+            )
+        }
+
+    override suspend fun updateToastCount(toastCount: Int) {
+        withContext(backgroundDispatcher) {
+            Prefs.putInt(applicationContext, Prefs.Key.SEEN_RINGER_GUIDANCE_COUNT, toastCount + 1)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index 281e57f..b83613b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.plugins.VolumeDialogController
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository
 import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
 import javax.inject.Inject
@@ -45,6 +46,7 @@
     volumeDialogStateInteractor: VolumeDialogStateInteractor,
     private val controller: VolumeDialogController,
     private val audioSystemRepository: AudioSystemRepository,
+    private val ringerFeedbackRepository: VolumeDialogRingerFeedbackRepository,
 ) {
 
     val ringerModel: Flow<VolumeDialogRingerModel> =
@@ -84,4 +86,12 @@
     fun scheduleTouchFeedback() {
         controller.scheduleTouchFeedback()
     }
+
+    suspend fun getToastCount(): Int {
+        return ringerFeedbackRepository.getToastCount()
+    }
+
+    suspend fun updateToastCount(toastCount: Int) {
+        ringerFeedbackRepository.updateToastCount(toastCount)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index d4da226..e040638 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -16,17 +16,23 @@
 
 package com.android.systemui.volume.dialog.ringer.ui.viewmodel
 
+import android.content.Context
 import android.media.AudioAttributes
 import android.media.AudioManager.RINGER_MODE_NORMAL
 import android.media.AudioManager.RINGER_MODE_SILENT
 import android.media.AudioManager.RINGER_MODE_VIBRATE
 import android.os.VibrationEffect
+import android.widget.Toast
+import com.android.internal.R as internalR
+import com.android.settingslib.Utils
 import com.android.settingslib.volume.shared.model.RingerMode
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.volume.Events
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
 import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor
 import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
 import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
@@ -40,26 +46,37 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+private const val SHOW_RINGER_TOAST_COUNT = 12
 
 class VolumeDialogRingerDrawerViewModel
 @AssistedInject
 constructor(
+    @Application private val applicationContext: Context,
     @VolumeDialog private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val interactor: VolumeDialogRingerInteractor,
     private val vibrator: VibratorHelper,
     private val volumeDialogLogger: VolumeDialogLogger,
+    private val visibilityInteractor: VolumeDialogVisibilityInteractor,
 ) {
 
     private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
 
     val ringerViewModel: StateFlow<RingerViewModelState> =
         combine(interactor.ringerModel, drawerState) { ringerModel, state ->
+                level = ringerModel.level
+                levelMax = ringerModel.levelMax
                 ringerModel.toViewModel(state)
             }
             .flowOn(backgroundDispatcher)
             .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
 
+    // Level and Maximum level of Ring Stream.
+    private var level = -1
+    private var levelMax = -1
+
     // Vibration attributes.
     private val sonificiationVibrationAttributes =
         AudioAttributes.Builder()
@@ -71,8 +88,10 @@
         if (drawerState.value is RingerDrawerState.Open) {
             Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
             provideTouchFeedback(ringerMode)
+            maybeShowToast(ringerMode)
             interactor.setRingerMode(ringerMode)
         }
+        visibilityInteractor.resetDismissTimeout()
         drawerState.value =
             when (drawerState.value) {
                 is RingerDrawerState.Initial -> {
@@ -201,6 +220,46 @@
         }
     }
 
+    private fun maybeShowToast(ringerMode: RingerMode) {
+        coroutineScope.launch {
+            val seenToastCount = interactor.getToastCount()
+            if (seenToastCount > SHOW_RINGER_TOAST_COUNT) {
+                return@launch
+            }
+
+            val toastText =
+                when (ringerMode.value) {
+                    RINGER_MODE_NORMAL -> {
+                        if (level != -1 && levelMax != -1) {
+                            applicationContext.getString(
+                                R.string.volume_dialog_ringer_guidance_ring,
+                                Utils.formatPercentage(level.toLong(), levelMax.toLong()),
+                            )
+                        } else {
+                            null
+                        }
+                    }
+
+                    RINGER_MODE_SILENT ->
+                        applicationContext.getString(
+                            internalR.string.volume_dialog_ringer_guidance_silent
+                        )
+
+                    RINGER_MODE_VIBRATE ->
+                        applicationContext.getString(
+                            internalR.string.volume_dialog_ringer_guidance_vibrate
+                        )
+
+                    else ->
+                        applicationContext.getString(
+                            internalR.string.volume_dialog_ringer_guidance_vibrate
+                        )
+                }
+            toastText?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT).show() }
+            interactor.updateToastCount(seenToastCount)
+        }
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): VolumeDialogRingerDrawerViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
index 53e6b4f..761993b 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
@@ -330,13 +330,19 @@
         QAWalletCardViewInfo(Context context, WalletCard walletCard) {
             mWalletCard = walletCard;
             Icon cardImageIcon = mWalletCard.getCardImage();
-            if (cardImageIcon.getType() == Icon.TYPE_URI) {
-                mCardDrawable = null;
-            } else {
+            if (cardImageIcon.getType() == Icon.TYPE_BITMAP
+                    || cardImageIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
                 mCardDrawable = mWalletCard.getCardImage().loadDrawable(context);
+            } else {
+                mCardDrawable = null;
             }
             Icon icon = mWalletCard.getCardIcon();
-            mIconDrawable = icon == null ? null : icon.loadDrawable(context);
+            if (icon != null && (icon.getType() == Icon.TYPE_BITMAP
+                    || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) {
+                mIconDrawable = icon.loadDrawable(context);
+            } else {
+                mIconDrawable = null;
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
index 1a39934..ca9b866 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
@@ -283,6 +283,11 @@
         return mCardLabel;
     }
 
+    @VisibleForTesting
+    ImageView getIcon() {
+        return mIcon;
+    }
+
     @Nullable
     private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) {
         Drawable icon = walletCard.getIcon();
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 8039e00..073781e 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -271,6 +271,12 @@
                         // No op.
                     }
                 }, mSysUiMainExecutor);
+        pip.addOnIsInPipStateChangedListener((isInPip) -> {
+            if (!isInPip) {
+                mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, false)
+                        .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+            }
+        });
         mSysUiState.addCallback(sysUiStateFlag -> {
             mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
             pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 2b167e4..65b6273 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -56,6 +56,7 @@
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.plugins.clocks.ZenData.ZenMode
 import com.android.systemui.res.R
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.ZenModeController
@@ -128,6 +129,7 @@
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
     @Mock private lateinit var parentView: View
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock private lateinit var userTracker: UserTracker
 
     @Mock private lateinit var zenModeController: ZenModeController
     private var zenModeControllerCallback: ZenModeController.Callback? = null
@@ -153,6 +155,7 @@
             .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE))
         whenever(smallClockController.theme).thenReturn(ThemeConfig(true, null))
         whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null))
+        whenever(userTracker.userId).thenReturn(1)
 
         zenModeRepository.addMode(MANUAL_DND_INACTIVE)
 
@@ -177,6 +180,7 @@
                 withDeps.featureFlags,
                 zenModeController,
                 kosmos.zenModeInteractor,
+                userTracker,
             )
         underTest.clock = clock
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 96f4a60..b4c6952 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -46,7 +46,6 @@
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -222,7 +221,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_NOTIFICATION_PULSING_FIX)
     public void testOnNotification_alreadyPulsing_notificationNotSuppressed() {
         // GIVEN device is pulsing
         Runnable pulseSuppressListener = mock(Runnable.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index fb376ce..3ddd4b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -289,6 +289,7 @@
             }
         verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
         mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
+        whenever(controller.sessionToken).thenReturn(session.sessionToken)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
@@ -1599,6 +1600,7 @@
         verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyCompactActions_isTruncated() {
         // GIVEN a notification where too many compact actions were specified
@@ -1635,6 +1637,7 @@
             .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyNotificationActions_isTruncated() {
         // GIVEN a notification where too many notification actions are added
@@ -1670,6 +1673,7 @@
             .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noState_usesNotification() {
         val desc = "Notification Action"
@@ -1703,6 +1707,7 @@
         assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_hasPrevNext() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1746,6 +1751,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noPrevNext_usesCustom() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
@@ -1778,6 +1784,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_connecting() {
         val stateActions = PlaybackState.ACTION_PLAY
@@ -1797,6 +1804,7 @@
             .isEqualTo(context.getString(R.string.controls_media_button_connecting))
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_reservedSpace() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1835,6 +1843,7 @@
         assertThat(actions.reservePrev).isTrue()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_playPause_hasButton() {
         val stateActions = PlaybackState.ACTION_PLAY_PAUSE
@@ -1998,6 +2007,7 @@
             assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
         }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackStateNull_Pause_keyExists_callsListener() {
         whenever(controller.playbackState).thenReturn(null)
@@ -2056,6 +2066,7 @@
         assertThat(mediaDataCaptor.value.isClearable).isFalse()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_notifRemoved_setToResume() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2086,6 +2097,7 @@
             )
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2104,6 +2116,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 7d364bd..e5483c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -103,7 +103,6 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -113,6 +112,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -312,6 +312,7 @@
             }
         verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
         mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
+        whenever(controller.sessionToken).thenReturn(session.sessionToken)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
@@ -596,7 +597,7 @@
     fun testOnNotificationAdded_emptyTitle_hasPlaceholder() {
         // When the manager has a notification with an empty title, and the app is not
         // required to include a non-empty title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -626,7 +627,7 @@
     fun testOnNotificationAdded_blankTitle_hasPlaceholder() {
         // GIVEN that the manager has a notification with a blank title, and the app is not
         // required to include a non-empty title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -656,7 +657,7 @@
     fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() {
         // When the app sets the metadata title fields to empty strings, but does include a
         // non-blank notification title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -1610,6 +1611,7 @@
         verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyCompactActions_isTruncated() {
         // GIVEN a notification where too many compact actions were specified
@@ -1646,6 +1648,7 @@
             .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyNotificationActions_isTruncated() {
         // GIVEN a notification where too many notification actions are added
@@ -1681,6 +1684,7 @@
             .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noState_usesNotification() {
         val desc = "Notification Action"
@@ -1714,6 +1718,7 @@
         assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_hasPrevNext() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1757,6 +1762,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noPrevNext_usesCustom() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
@@ -1789,6 +1795,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_connecting() {
         val stateActions = PlaybackState.ACTION_PLAY
@@ -1874,6 +1881,7 @@
             .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_reservedSpace() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1912,6 +1920,7 @@
         assertThat(actions.reservePrev).isTrue()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_playPause_hasButton() {
         val stateActions = PlaybackState.ACTION_PLAY_PAUSE
@@ -2074,6 +2083,7 @@
         assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackStateNull_Pause_keyExists_callsListener() {
         whenever(controller.playbackState).thenReturn(null)
@@ -2132,6 +2142,7 @@
         assertThat(mediaDataCaptor.value.isClearable).isFalse()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_notifRemoved_setToResume() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2162,6 +2173,7 @@
             )
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2180,6 +2192,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 4b648a3..d1e4f64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -17,7 +17,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS;
-import static com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
 
@@ -61,6 +60,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
+import com.android.systemui.statusbar.core.StatusBarRootModernization;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
@@ -156,7 +156,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableNone() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -167,7 +167,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableSystemInfo_systemAnimationIdle_doesHide() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -185,7 +185,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testSystemStatusAnimation_startedDisabled_finishedWithAnimator_showsSystemInfo() {
         // GIVEN the status bar hides the system info via disable flags, while there is no event
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -215,7 +215,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testSystemStatusAnimation_systemInfoDisabled_staysInvisible() {
         // GIVEN the status bar hides the system info via disable flags, while there is no event
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -232,7 +232,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testSystemStatusAnimation_notDisabled_animatesAlphaZero() {
         // GIVEN the status bar is not disabled
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -248,7 +248,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testSystemStatusAnimation_notDisabled_animatesBackToAlphaOne() {
         // GIVEN the status bar is not disabled
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
@@ -272,7 +272,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableNotifications() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -290,7 +290,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @EnableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableNotifications_doesNothingWhenFlagEnabled() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -308,7 +308,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableClock() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -326,7 +326,7 @@
     }
 
     @Test
-    @EnableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @EnableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testDisableClock_doesNothingWhenFlagEnabled() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -345,7 +345,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_shadeOpenAndShouldHide_everythingHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -363,7 +363,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_shadeOpenButNotShouldHide_everythingShown() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -382,7 +382,7 @@
     /** Regression test for b/279790651. */
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_shadeOpenAndShouldHide_thenShadeNotOpenAndDozingUpdate_everythingShown() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -410,7 +410,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_notTransitioningToOccluded_everythingShown() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -426,7 +426,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_isTransitioningToOccluded_everythingHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -442,7 +442,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_wasTransitioningToOccluded_transitionFinished_everythingShown() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -473,7 +473,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_noOngoingCall_chipHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -485,7 +485,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_hasOngoingCall_chipDisplayedAndNotificationIconsHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -498,7 +498,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_hasOngoingCallButNotificationIconsDisabled_chipHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -511,7 +511,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_hasOngoingCallButAlsoHun_chipHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -524,7 +524,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_ongoingCallEnded_chipHidden() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -548,7 +548,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
@@ -565,7 +565,7 @@
     }
 
     @Test
-    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarRootModernization.FLAG_NAME})
     public void screenSharingChipsDisabled_ignoresNewCallback() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -599,7 +599,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void noOngoingActivity_chipHidden() {
         resumeAndGetFragment();
 
@@ -617,7 +617,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void hasPrimaryOngoingActivity_primaryChipDisplayedAndNotificationIconsHidden() {
         resumeAndGetFragment();
 
@@ -634,8 +634,8 @@
     @EnableFlags({
             FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
             StatusBarNotifChips.FLAG_NAME,
-            FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
-    public void hasPrimaryOngoingActivity_viewsUnchangedWhenSimpleFragmentFlagOn() {
+            StatusBarRootModernization.FLAG_NAME})
+    public void hasPrimaryOngoingActivity_viewsUnchangedWhenRootModernizationFlagOn() {
         resumeAndGetFragment();
 
         assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility());
@@ -660,7 +660,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void hasSecondaryOngoingActivity_butNotifsFlagOff_secondaryChipHidden() {
         resumeAndGetFragment();
 
@@ -674,7 +674,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() {
         resumeAndGetFragment();
 
@@ -689,7 +689,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -706,7 +706,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -724,7 +724,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void hasOngoingActivityButAlsoHun_chipHidden_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -741,7 +741,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -759,7 +759,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void primaryOngoingActivityEnded_chipHidden_notifsFlagOff() {
         resumeAndGetFragment();
 
@@ -782,7 +782,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() {
         resumeAndGetFragment();
 
@@ -805,7 +805,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void secondaryOngoingActivityEnded_chipHidden() {
         resumeAndGetFragment();
 
@@ -828,7 +828,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
@@ -847,7 +847,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         // Enable animations for testing so that we can verify we still aren't animating
@@ -866,7 +866,7 @@
 
     @Test
     @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
-    @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+    @DisableFlags({StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME})
     public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -899,7 +899,7 @@
 
     @Test
     @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -933,7 +933,7 @@
 
     @Test
     @EnableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void isHomeStatusBarAllowedByScene_false_everythingHidden() {
         resumeAndGetFragment();
 
@@ -947,7 +947,7 @@
 
     @Test
     @EnableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void isHomeStatusBarAllowedByScene_true_everythingShown() {
         resumeAndGetFragment();
 
@@ -961,7 +961,7 @@
 
     @Test
     @EnableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_isHomeStatusBarAllowedBySceneFalse_disableValuesIgnored() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -979,7 +979,7 @@
 
     @Test
     @EnableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_isHomeStatusBarAllowedBySceneTrue_disableValuesUsed() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -997,7 +997,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void isHomeStatusBarAllowedByScene_sceneContainerDisabled_valueNotUsed() {
         resumeAndGetFragment();
 
@@ -1011,7 +1011,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_isDozing_clockAndSystemInfoVisible() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mStatusBarStateController.isDozing()).thenReturn(true);
@@ -1023,7 +1023,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_NotDozing_clockAndSystemInfoVisible() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mStatusBarStateController.isDozing()).thenReturn(false);
@@ -1035,7 +1035,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_headsUpShouldBeVisibleTrue_clockDisabled() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
@@ -1046,7 +1046,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void disable_headsUpShouldBeVisibleFalse_clockNotDisabled() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(false);
@@ -1100,7 +1100,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testStatusBarIcons_hiddenThroughoutCameraLaunch() {
         final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -1123,7 +1123,7 @@
 
     @Test
     @DisableSceneContainer
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testStatusBarIcons_hiddenThroughoutLockscreenToDreamTransition() {
         final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
@@ -1159,7 +1159,7 @@
     }
 
     @Test
-    @DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+    @DisableFlags(StatusBarRootModernization.FLAG_NAME)
     public void testStatusBarIcons_lockscreenToDreamTransitionButNotDreaming_iconsVisible() {
         final CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
index c435d3d..37671e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
@@ -21,9 +21,9 @@
 import android.testing.TestableLooper
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.AnimatorTestRule
+import com.android.systemui.statusbar.core.StatusBarRootModernization
 import junit.framework.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -38,7 +38,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
-@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
+@DisableFlags(StatusBarRootModernization.FLAG_NAME)
 class MultiSourceMinAlphaControllerTest : SysuiTestCase() {
 
     private val view = View(context)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index c48898a..2e0b7c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -67,12 +67,12 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
 import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.wifitrackerlib.MergedCarrierEntry
 import com.android.wifitrackerlib.WifiEntry
@@ -96,6 +96,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@@ -137,6 +139,7 @@
     private val wifiPickerTrackerCallback =
         argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
     private val vcnTransportInfo = VcnTransportInfo.Builder().build()
+    private val userRepository = kosmos.fakeUserRepository
 
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
@@ -159,7 +162,14 @@
             logcatTableLogBuffer(kosmos, "test")
         }
 
-        whenever(wifiPickerTrackerFactory.create(any(), capture(wifiPickerTrackerCallback), any()))
+        whenever(
+                wifiPickerTrackerFactory.create(
+                    any(),
+                    any(),
+                    capture(wifiPickerTrackerCallback),
+                    any(),
+                )
+            )
             .thenReturn(wifiPickerTracker)
 
         // For convenience, set up the subscription info callbacks
@@ -188,6 +198,8 @@
 
         wifiRepository =
             WifiRepositoryImpl(
+                mContext,
+                userRepository,
                 testScope.backgroundScope,
                 mainExecutor,
                 testDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 44e1437..d823bf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -16,13 +16,19 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
 
+import android.content.Context
+import android.content.pm.UserInfo
 import android.net.wifi.ScanResult
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.UNKNOWN_SSID
 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo
+import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.log.LogBuffer
@@ -33,12 +39,10 @@
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
 import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.fakeSystemClock
 import com.android.wifitrackerlib.HotspotNetworkEntry
 import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType
@@ -56,7 +60,12 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
-import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 /**
  * Note: Most of these tests are duplicates of [WifiRepositoryImplTest] tests.
@@ -67,8 +76,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-class WifiRepositoryImplTest : SysuiTestCase() {
+class WifiRepositoryImplTest() : SysuiTestCase() {
     private val kosmos = testKosmos()
+    private val userRepository = kosmos.fakeUserRepository
 
     // Using lazy means that the class will only be constructed once it's fetched. Because the
     // repository internally sets some values on construction, we need to set up some test
@@ -76,6 +86,8 @@
     // inside each test case without needing to manually recreate the repository.
     private val underTest: WifiRepositoryImpl by lazy {
         WifiRepositoryImpl(
+            mContext,
+            userRepository,
             testScope.backgroundScope,
             executor,
             dispatcher,
@@ -101,7 +113,8 @@
 
     @Before
     fun setUp() {
-        whenever(wifiPickerTrackerFactory.create(any(), capture(callbackCaptor), any()))
+        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+        whenever(wifiPickerTrackerFactory.create(any(), any(), capture(callbackCaptor), any()))
             .thenReturn(wifiPickerTracker)
     }
 
@@ -1203,6 +1216,95 @@
             assertThat(latest).isEmpty()
         }
 
+    // TODO(b/371586248): This test currently require currentUserContext to be public for testing,
+    // this needs to
+    // be updated to capture the argument instead so currentUserContext can be private.
+    @Test
+    @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT)
+    fun oneUserVerifyCreatingWifiPickerTracker_multiuserFlagEnabled() =
+        testScope.runTest {
+            val primaryUserMockContext = mock<Context>()
+            mContext.prepareCreateContextAsUser(
+                UserHandle.of(PRIMARY_USER_ID),
+                primaryUserMockContext,
+            )
+
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            runCurrent()
+            val currentUserContext by collectLastValue(underTest.selectedUserContext)
+
+            assertThat(currentUserContext).isEqualTo(primaryUserMockContext)
+            verify(wifiPickerTrackerFactory).create(any(), any(), any(), any())
+        }
+
+    // TODO(b/371586248): This test currently require currentUserContext to be public for testing,
+    // this needs to
+    // be updated to capture the argument instead so currentUserContext can be private.
+    @Test
+    @EnableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT)
+    fun changeUserVerifyCreatingWifiPickerTracker_multiuserEnabled() =
+        testScope.runTest {
+            val primaryUserMockContext = mock<Context>()
+            mContext.prepareCreateContextAsUser(
+                UserHandle.of(PRIMARY_USER_ID),
+                primaryUserMockContext,
+            )
+
+            runCurrent()
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            runCurrent()
+            val currentUserContext by collectLastValue(underTest.selectedUserContext)
+
+            assertThat(currentUserContext).isEqualTo(primaryUserMockContext)
+
+            val otherUserMockContext = mock<Context>()
+            mContext.prepareCreateContextAsUser(
+                UserHandle.of(ANOTHER_USER_ID),
+                otherUserMockContext,
+            )
+
+            runCurrent()
+            userRepository.setSelectedUserInfo(ANOTHER_USER)
+            runCurrent()
+            val otherUserContext by collectLastValue(underTest.selectedUserContext)
+
+            assertThat(otherUserContext).isEqualTo(otherUserMockContext)
+            verify(wifiPickerTrackerFactory, times(2)).create(any(), any(), any(), any())
+        }
+
+    // TODO(b/371586248): This test currently require currentUserContext to be public for testing,
+    // this needs to
+    // be updated to capture the argument instead so currentUserContext can be private.
+    @Test
+    @DisableFlags(FLAG_MULTIUSER_WIFI_PICKER_TRACKER_SUPPORT)
+    fun changeUserVerifyCreatingWifiPickerTracker_multiuserDisabled() =
+        testScope.runTest {
+            val primaryUserMockContext = mock<Context>()
+            mContext.prepareCreateContextAsUser(
+                UserHandle.of(PRIMARY_USER_ID),
+                primaryUserMockContext,
+            )
+
+            runCurrent()
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            runCurrent()
+            val currentUserContext by collectLastValue(underTest.selectedUserContext)
+
+            assertThat(currentUserContext).isEqualTo(primaryUserMockContext)
+
+            val otherUserMockContext = mock<Context>()
+            mContext.prepareCreateContextAsUser(
+                UserHandle.of(ANOTHER_USER_ID),
+                otherUserMockContext,
+            )
+
+            runCurrent()
+            userRepository.setSelectedUserInfo(ANOTHER_USER)
+            runCurrent()
+
+            verify(wifiPickerTrackerFactory, times(1)).create(any(), any(), any(), any())
+        }
+
     private fun getCallback(): WifiPickerTracker.WifiPickerTrackerCallback {
         testScope.runCurrent()
         return callbackCaptor.value
@@ -1231,5 +1333,20 @@
 
     private companion object {
         const val TITLE = "AB"
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PROFILE,
+            )
+
+        private const val ANOTHER_USER_ID = 1
+        private val ANOTHER_USER =
+            UserInfo(
+                /* id= */ ANOTHER_USER_ID,
+                /* name= */ "another user",
+                /* flags= */ UserInfo.FLAG_PROFILE,
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
index 38a61fe..21adeb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
@@ -316,6 +316,31 @@
     }
 
     @Test
+    public void queryCards_hasCards_showCarousel_invalidIconSource_noIcon() {
+        GetWalletCardsResponse response =
+                new GetWalletCardsResponse(
+                        Collections.singletonList(createWalletCardWithInvalidIcon(mContext)), 0);
+
+        mController.queryWalletCards();
+        mTestableLooper.processAllMessages();
+
+        verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
+
+        QuickAccessWalletClient.OnWalletCardsRetrievedCallback callback =
+                mCallbackCaptor.getValue();
+
+        assertEquals(mController, callback);
+
+        callback.onWalletCardsRetrieved(response);
+        mTestableLooper.processAllMessages();
+
+        assertEquals(VISIBLE, mWalletView.getCardCarousel().getVisibility());
+        assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility());
+        assertEquals(GONE, mWalletView.getErrorView().getVisibility());
+        assertEquals(null, mWalletView.getIcon().getDrawable());
+    }
+
+    @Test
     public void queryCards_noCards_showEmptyState() {
         GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
 
@@ -507,6 +532,16 @@
                 .build();
     }
 
+    private WalletCard createWalletCardWithInvalidIcon(Context context) {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
+        return new WalletCard.Builder(
+                CARD_ID_1, createIconWithInvalidSource(), "•••• 1234", pendingIntent)
+                .setCardIcon(createIconWithInvalidSource())
+                .setCardLabel("Hold to reader")
+                .build();
+    }
+
     private WalletCard createCrazyWalletCard(Context context, boolean hasLabel) {
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
@@ -520,6 +555,10 @@
         return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888));
     }
 
+    private static Icon createIconWithInvalidSource() {
+        return Icon.createWithContentUri("content://media/external/images/media");
+    }
+
     private WalletCardViewInfo createCardViewInfo(WalletCard walletCard) {
         return new WalletScreenController.QAWalletCardViewInfo(
                 mContext, walletCard);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 219794f..a7917a0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -37,9 +37,7 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.currentTime
 
-class FakeAuthenticationRepository(
-    private val currentTime: () -> Long,
-) : AuthenticationRepository {
+class FakeAuthenticationRepository(private val currentTime: () -> Long) : AuthenticationRepository {
 
     override val hintedPinLength: Int = HINTING_PIN_LENGTH
 
@@ -72,6 +70,9 @@
 
     private val credentialCheckingMutex = Mutex(locked = false)
 
+    var maximumTimeToLock: Long = 0
+    var powerButtonInstantlyLocks: Boolean = true
+
     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
         return authenticationMethod.value
     }
@@ -114,6 +115,7 @@
         MAX_FAILED_AUTH_TRIES_BEFORE_WIPE
 
     var profileWithMinFailedUnlockAttemptsForWipe: Int = UserHandle.USER_SYSTEM
+
     override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int =
         profileWithMinFailedUnlockAttemptsForWipe
 
@@ -144,10 +146,7 @@
 
             val failedAttempts = _failedAuthenticationAttempts.value
             if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-                AuthenticationResultModel(
-                    isSuccessful = isSuccessful,
-                    lockoutDurationMs = 0,
-                )
+                AuthenticationResultModel(isSuccessful = isSuccessful, lockoutDurationMs = 0)
             } else {
                 AuthenticationResultModel(
                     isSuccessful = false,
@@ -178,6 +177,14 @@
         credentialCheckingMutex.unlock()
     }
 
+    override suspend fun getMaximumTimeToLock(): Long {
+        return maximumTimeToLock
+    }
+
+    override suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return powerButtonInstantlyLocks
+    }
+
     private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
         return when (val credentialType = getCurrentCredentialType(securityMode)) {
             LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN
@@ -219,9 +226,7 @@
         }
 
         @LockPatternUtils.CredentialType
-        private fun getCurrentCredentialType(
-            securityMode: SecurityMode,
-        ): Int {
+        private fun getCurrentCredentialType(securityMode: SecurityMode): Int {
             return when (securityMode) {
                 SecurityMode.PIN,
                 SecurityMode.SimPin,
@@ -260,9 +265,8 @@
 object FakeAuthenticationRepositoryModule {
     @Provides
     @SysUISingleton
-    fun provideFake(
-        scope: TestScope,
-    ) = FakeAuthenticationRepository(currentTime = { scope.currentTime })
+    fun provideFake(scope: TestScope) =
+        FakeAuthenticationRepository(currentTime = { scope.currentTime })
 
     @Module
     interface Bindings {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index 2dcd275..f6ff4c4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.deviceentry.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
@@ -35,6 +36,9 @@
 
     private var pendingLockscreenEnabled = _isLockscreenEnabled.value
 
+    override val deviceUnlockStatus =
+        MutableStateFlow(DeviceUnlockStatus(isUnlocked = false, deviceUnlockSource = null))
+
     override suspend fun isLockscreenEnabled(): Boolean {
         _isLockscreenEnabled.value = pendingLockscreenEnabled
         return isLockscreenEnabled.value
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
index 8922b2f..e4c7df6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -19,25 +19,27 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.flags.fakeSystemPropertiesHelper
-import com.android.systemui.flags.systemPropertiesHelper
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.trustInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 
 val Kosmos.deviceUnlockedInteractor by Fixture {
     DeviceUnlockedInteractor(
-        applicationScope = applicationCoroutineScope,
-        authenticationInteractor = authenticationInteractor,
-        deviceEntryRepository = deviceEntryRepository,
-        trustInteractor = trustInteractor,
-        faceAuthInteractor = deviceEntryFaceAuthInteractor,
-        fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
-        powerInteractor = powerInteractor,
-        biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
-        systemPropertiesHelper = fakeSystemPropertiesHelper,
-        keyguardTransitionInteractor = keyguardTransitionInteractor,
-    )
+            authenticationInteractor = authenticationInteractor,
+            repository = deviceEntryRepository,
+            trustInteractor = trustInteractor,
+            faceAuthInteractor = deviceEntryFaceAuthInteractor,
+            fingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
+            powerInteractor = powerInteractor,
+            biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
+            systemPropertiesHelper = fakeSystemPropertiesHelper,
+            userAwareSecureSettingsRepository = userAwareSecureSettingsRepository,
+            keyguardInteractor = keyguardInteractor,
+        )
+        .apply { activateIn(testScope) }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 19e077c..8209ee1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -87,7 +87,7 @@
     ) : this(
         initInLockscreen = true,
         initiallySendTransitionStepsOnStartTransition = true,
-        testScope
+        testScope,
     )
 
     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
@@ -191,12 +191,12 @@
         if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) {
             sendTransitionStep(
                 step =
-                TransitionStep(
-                    transitionState = TransitionState.CANCELED,
-                    from = lastStep.from,
-                    to = lastStep.to,
-                    value = 0f,
-                )
+                    TransitionStep(
+                        transitionState = TransitionState.CANCELED,
+                        from = lastStep.from,
+                        to = lastStep.to,
+                        value = 0f,
+                    )
             )
             testScheduler.runCurrent()
         }
@@ -390,6 +390,18 @@
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState,
     ) = Unit
+
+    override suspend fun forceFinishCurrentTransition() {
+        _transitions.tryEmit(
+            TransitionStep(
+                _currentTransitionInfo.value.from,
+                _currentTransitionInfo.value.to,
+                1f,
+                TransitionState.FINISHED,
+                ownerName = _currentTransitionInfo.value.ownerName,
+            )
+        )
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index aa94c36..b9a831f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 
 val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by
@@ -26,6 +27,7 @@
         KeyguardTransitionInteractor(
             scope = applicationCoroutineScope,
             repository = keyguardTransitionRepository,
-            sceneInteractor = sceneInteractor
+            sceneInteractor = sceneInteractor,
+            powerInteractor = powerInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
index f49e377..b3be2c0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
@@ -22,6 +22,7 @@
 import android.os.looper
 import androidx.media3.session.CommandButton
 import androidx.media3.session.MediaController
+import androidx.media3.session.SessionCommand
 import androidx.media3.session.SessionToken
 import com.android.systemui.Flags
 import com.android.systemui.graphics.imageLoader
@@ -30,7 +31,11 @@
 import com.android.systemui.media.controls.shared.mediaLogger
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.fakeSessionTokenFactory
+import com.android.systemui.util.concurrency.execution
 import com.google.common.collect.ImmutableList
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -46,10 +51,22 @@
                 mock<MediaController>().also {
                     whenever(it.customLayout).thenReturn(customLayout)
                     whenever(it.sessionExtras).thenReturn(Bundle())
+                    whenever(it.isCommandAvailable(any())).thenReturn(true)
+                    whenever(it.isSessionCommandAvailable(any<SessionCommand>())).thenReturn(true)
                 }
             fakeMediaControllerFactory.setMedia3Controller(media3Controller)
             fakeSessionTokenFactory.setMedia3SessionToken(mock<SessionToken>())
         }
+
+        val runnableCaptor = argumentCaptor<Runnable>()
+        val handler =
+            mock<Handler> {
+                on { post(runnableCaptor.capture()) } doAnswer
+                    {
+                        runnableCaptor.lastValue.run()
+                        true
+                    }
+            }
         Media3ActionFactory(
             context = applicationContext,
             imageLoader = imageLoader,
@@ -57,7 +74,8 @@
             tokenFactory = fakeSessionTokenFactory,
             logger = mediaLogger,
             looper = looper,
-            handler = Handler(looper),
+            handler = handler,
             bgScope = testScope,
+            execution = execution,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt
index 513d4e4..1395b18 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryKosmos.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.qs.panels.data.repository
 
+import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.settings.userFileManager
 import com.android.systemui.user.data.repository.userRepository
 
@@ -27,6 +29,8 @@
             userFileManager,
             userRepository,
             defaultLargeTilesRepository,
-            testDispatcher
+            testDispatcher,
+            FakeLogBuffer.Factory.create(),
+            broadcastDispatcher,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt
index d1b613f..f63698a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runCurrent
 import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager
 import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
 import com.android.systemui.shade.data.repository.shadeRepository
@@ -56,4 +57,5 @@
         }
     mainResources.configuration.updateFrom(config)
     fakeConfigurationRepository.onConfigurationChange(config)
+    runCurrent()
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
index 45aab86..28edae7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
@@ -49,6 +49,7 @@
             fakeStatusBarModePerDisplayRepository,
             fakeStatusBarInitializer,
             fakeStatusBarWindowController,
+            applicationCoroutineScope.coroutineContext,
             mockDemoModeController,
             mockPluginDependencyProvider,
             mockAutoHideController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index c3c3cce..dae66d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -41,6 +41,7 @@
     override val isGsm = MutableStateFlow(false)
     override val cdmaLevel = MutableStateFlow(0)
     override val primaryLevel = MutableStateFlow(0)
+    override val satelliteLevel = MutableStateFlow(0)
     override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected)
     override val dataActivityDirection =
         MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 1808a5f..85d582a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -72,6 +72,10 @@
     override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> =
         _isSecondaryUserLogoutEnabled.asStateFlow()
 
+    private val _isLogoutToSystemUserEnabled = MutableStateFlow<Boolean>(false)
+    override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
+        _isLogoutToSystemUserEnabled.asStateFlow()
+
     override var mainUserId: Int = MAIN_USER_ID
     override var lastSelectedNonGuestUserId: Int = mainUserId
 
@@ -123,6 +127,17 @@
         logOutSecondaryUserCallCount++
     }
 
+    fun setLogoutToSystemUserEnabled(logoutEnabled: Boolean) {
+        _isLogoutToSystemUserEnabled.value = logoutEnabled
+    }
+
+    var logOutToSystemUserCallCount: Int = 0
+        private set
+
+    override suspend fun logOutToSystemUser() {
+        logOutToSystemUserCallCount++
+    }
+
     fun setUserInfos(infos: List<UserInfo>) {
         _userInfos.value = infos
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt
new file mode 100644
index 0000000..d42de1e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/FakeVolumeDialogRingerFeedbackRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.volume.dialog.ringer.data.repository
+
+class FakeVolumeDialogRingerFeedbackRepository : VolumeDialogRingerFeedbackRepository {
+
+    private var seenToastCount = 0
+
+    override suspend fun getToastCount(): Int {
+        return seenToastCount
+    }
+
+    override suspend fun updateToastCount(toastCount: Int) {
+        seenToastCount = toastCount
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt
new file mode 100644
index 0000000..44371b4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.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.volume.dialog.ringer.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeVolumeDialogRingerFeedbackRepository by
+    Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() }
+val Kosmos.volumeDialogRingerFeedbackRepository by
+    Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index 1addd91..a494d04 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.plugins.volumeDialogController
 import com.android.systemui.volume.data.repository.audioSystemRepository
 import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
+import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository
 
 val Kosmos.volumeDialogRingerInteractor by
     Kosmos.Fixture {
@@ -29,5 +30,6 @@
             volumeDialogStateInteractor = volumeDialogStateInteractor,
             controller = volumeDialogController,
             audioSystemRepository = audioSystemRepository,
+            ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
index db1c01a..c8ba551 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
@@ -16,20 +16,24 @@
 
 package com.android.systemui.volume.dialog.ringer.ui.viewmodel
 
+import android.content.applicationContext
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor
 import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor
 import com.android.systemui.volume.dialog.shared.volumeDialogLogger
 
 val Kosmos.volumeDialogRingerDrawerViewModel by
     Kosmos.Fixture {
         VolumeDialogRingerDrawerViewModel(
+            applicationContext = applicationContext,
             backgroundDispatcher = testDispatcher,
             coroutineScope = applicationCoroutineScope,
             interactor = volumeDialogRingerInteractor,
             vibrator = vibratorHelper,
             volumeDialogLogger = volumeDialogLogger,
+            visibilityInteractor = volumeDialogVisibilityInteractor,
         )
     }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
index d8f2b70..3ed8b0a 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
@@ -16,7 +16,6 @@
 package android.platform.test.ravenwood;
 
 import static android.os.Process.FIRST_APPLICATION_UID;
-import static android.os.Process.SYSTEM_UID;
 import static android.os.UserHandle.SYSTEM;
 
 import android.annotation.NonNull;
@@ -61,17 +60,14 @@
      * Unless the test author requests differently, run as "nobody", and give each collection of
      * tests its own unique PID.
      */
-    int mUid = NOBODY_UID;
+    int mUid = FIRST_APPLICATION_UID;
     int mPid = sNextPid.getAndIncrement();
 
     String mTestPackageName;
     String mTargetPackageName;
 
-    int mMinSdkLevel;
     int mTargetSdkLevel = Build.VERSION_CODES.CUR_DEVELOPMENT;
 
-    boolean mProvideMainThread = false;
-
     final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
 
     final List<Class<?>> mServicesRequired = new ArrayList<>();
@@ -108,20 +104,18 @@
         }
 
         /**
-         * Configure the identity of this process to be the system UID for the duration of the
-         * test. Has no effect on non-Ravenwood environments.
+         * @deprecated no longer used. We always use an app UID.
          */
+        @Deprecated
         public Builder setProcessSystem() {
-            mConfig.mUid = SYSTEM_UID;
             return this;
         }
 
         /**
-         * Configure the identity of this process to be an app UID for the duration of the
-         * test. Has no effect on non-Ravenwood environments.
+         * @deprecated no longer used. We always use an app UID.
          */
+        @Deprecated
         public Builder setProcessApp() {
-            mConfig.mUid = FIRST_APPLICATION_UID;
             return this;
         }
 
@@ -144,14 +138,6 @@
         }
 
         /**
-         * Configure the min SDK level of the test.
-         */
-        public Builder setMinSdkLevel(int sdkLevel) {
-            mConfig.mMinSdkLevel = sdkLevel;
-            return this;
-        }
-
-        /**
          * Configure the target SDK level of the test.
          */
         public Builder setTargetSdkLevel(int sdkLevel) {
@@ -160,14 +146,10 @@
         }
 
         /**
-         * Configure a "main" thread to be available for the duration of the test, as defined
-         * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
-         *
-         * @deprecated
+         * @deprecated no longer used. Main thread is always available.
          */
         @Deprecated
         public Builder setProvideMainThread(boolean provideMainThread) {
-            mConfig.mProvideMainThread = provideMainThread;
             return this;
         }
 
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 3d6ac0f..bfa3802 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -112,20 +112,18 @@
         }
 
         /**
-         * Configure the identity of this process to be the system UID for the duration of the
-         * test. Has no effect on non-Ravenwood environments.
+         * @deprecated no longer used. We always use an app UID.
          */
+        @Deprecated
         public Builder setProcessSystem() {
-            mBuilder.setProcessSystem();
             return this;
         }
 
         /**
-         * Configure the identity of this process to be an app UID for the duration of the
-         * test. Has no effect on non-Ravenwood environments.
+         * @deprecated no longer used. We always use an app UID.
          */
+        @Deprecated
         public Builder setProcessApp() {
-            mBuilder.setProcessApp();
             return this;
         }
 
@@ -139,14 +137,10 @@
         }
 
         /**
-         * Configure a "main" thread to be available for the duration of the test, as defined
-         * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
-         *
-         * @deprecated
+         * @deprecated no longer used. Main thread is always available.
          */
         @Deprecated
         public Builder setProvideMainThread(boolean provideMainThread) {
-            mBuilder.setProvideMainThread(provideMainThread);
             return this;
         }
 
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
index d232ef2..c85bd23 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
@@ -18,6 +18,7 @@
 import android.util.Log.Level;
 
 import com.android.internal.os.RuntimeInit;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import java.io.PrintStream;
 
@@ -35,6 +36,9 @@
     }
 
     public static int println_native(int bufID, int priority, String tag, String msg) {
+        if (priority < Log.INFO && !RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING) {
+            return msg.length(); // No verbose logging.
+        }
         final String buffer;
         switch (bufID) {
             case Log.LOG_ID_MAIN: buffer = "main"; break;
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index 82be2c0..c196a09 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -15,7 +15,9 @@
 com.android.internal.os.BatteryStatsHistoryIterator
 com.android.internal.os.Clock
 com.android.internal.os.LongArrayMultiStateCounter
+com.android.internal.os.LongArrayMultiStateCounter_ravenwood
 com.android.internal.os.LongMultiStateCounter
+com.android.internal.os.LongMultiStateCounter_ravenwood
 com.android.internal.os.MonotonicClock
 com.android.internal.os.PowerProfile
 com.android.internal.os.PowerStats
@@ -143,6 +145,7 @@
 android.os.Looper
 android.os.Message
 android.os.MessageQueue
+android.os.MessageQueue_ravenwood
 android.os.PackageTagsList
 android.os.Parcel
 android.os.ParcelFileDescriptor
@@ -235,6 +238,7 @@
 android.database.CursorIndexOutOfBoundsException
 android.database.CursorJoiner
 android.database.CursorWindow
+android.database.CursorWindow_ravenwood
 android.database.CursorWrapper
 android.database.DataSetObservable
 android.database.DataSetObserver
@@ -370,4 +374,3 @@
 com.android.server.compat.*
 com.android.internal.compat.*
 android.app.AppCompatCallbacks
-
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 617cca9..d6fc6e4 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -27,6 +27,8 @@
 import android.content.Context;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
+import android.hardware.input.KeyGestureEvent;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemClock;
@@ -44,11 +46,14 @@
 import android.view.MotionEvent.PointerProperties;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.annotation.Nullable;
+
 import com.android.server.LocalServices;
 import com.android.server.accessibility.gestures.TouchExplorer;
 import com.android.server.accessibility.magnification.FullScreenMagnificationController;
 import com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandler;
 import com.android.server.accessibility.magnification.FullScreenMagnificationVibrationHelper;
+import com.android.server.accessibility.magnification.MagnificationController;
 import com.android.server.accessibility.magnification.MagnificationGestureHandler;
 import com.android.server.accessibility.magnification.MouseEventHandler;
 import com.android.server.accessibility.magnification.WindowMagnificationGestureHandler;
@@ -187,6 +192,8 @@
 
     private final AccessibilityManagerService mAms;
 
+    private final InputManager mInputManager;
+
     private final SparseArray<EventStreamTransformation> mEventHandler;
 
     private final SparseArray<TouchExplorer> mTouchExplorer = new SparseArray<>(0);
@@ -228,6 +235,47 @@
      */
     private MotionEvent mLastActiveDeviceMotionEvent = null;
 
+    private boolean mKeyGestureEventHandlerInstalled = false;
+    private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
+            new InputManager.KeyGestureEventHandler() {
+                @Override
+                public boolean handleKeyGestureEvent(
+                        @NonNull KeyGestureEvent event,
+                        @Nullable IBinder focusedToken) {
+                    final boolean complete =
+                            event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                                    && !event.isCancelled();
+                    final int gestureType = event.getKeyGestureType();
+                    final int displayId = isDisplayIdValid(event.getDisplayId())
+                            ? event.getDisplayId() : Display.DEFAULT_DISPLAY;
+
+                    switch (gestureType) {
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN:
+                            if (complete) {
+                                mAms.getMagnificationController().scaleMagnificationByStep(
+                                        displayId, MagnificationController.ZOOM_DIRECTION_IN);
+                            }
+                            return true;
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
+                            if (complete) {
+                                mAms.getMagnificationController().scaleMagnificationByStep(
+                                        displayId, MagnificationController.ZOOM_DIRECTION_OUT);
+                            }
+                            return true;
+                    }
+                    return false;
+                }
+
+                @Override
+                public boolean isKeyGestureSupported(int gestureType) {
+                    return switch (gestureType) {
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT -> true;
+                        default -> false;
+                    };
+                }
+            };
+
     private static MotionEvent cancelMotion(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
                 || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
@@ -287,6 +335,7 @@
         mContext = context;
         mAms = service;
         mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mInputManager = context.getSystemService(InputManager.class);
         mEventHandler = eventHandler;
     }
 
@@ -723,6 +772,12 @@
                     createMagnificationGestureHandler(displayId, displayContext);
             addFirstEventHandler(displayId, magnificationGestureHandler);
             mMagnificationGestureHandler.put(displayId, magnificationGestureHandler);
+
+            if (com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures()
+                    && !mKeyGestureEventHandlerInstalled) {
+                mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
+                mKeyGestureEventHandlerInstalled = true;
+            }
         }
 
         if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
@@ -842,6 +897,11 @@
             mMouseKeysInterceptor.onDestroy();
             mMouseKeysInterceptor = null;
         }
+
+        if (mKeyGestureEventHandlerInstalled) {
+            mInputManager.unregisterKeyGestureEventHandler(mKeyGestureEventHandler);
+            mKeyGestureEventHandlerInstalled = false;
+        }
     }
 
     private MagnificationGestureHandler createMagnificationGestureHandler(
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 974cba2..d4af7b7 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -42,9 +42,11 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
+import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
 
+import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
@@ -55,6 +57,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -111,6 +114,8 @@
 import android.graphics.Region;
 import android.hardware.display.DisplayManager;
 import android.hardware.fingerprint.IFingerprintService;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyGestureEvent;
 import android.media.AudioManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
@@ -338,6 +343,8 @@
 
     private AlertDialog mEnableTouchExplorationDialog;
 
+    private final InputManager mInputManager;
+
     private AccessibilityInputFilter mInputFilter;
 
     private boolean mHasInputFilter;
@@ -503,6 +510,25 @@
         }
     }
 
+    private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
+            new InputManager.KeyGestureEventHandler() {
+                @Override
+                public boolean handleKeyGestureEvent(
+                        @NonNull KeyGestureEvent event,
+                        @Nullable IBinder focusedToken) {
+                    return AccessibilityManagerService.this.handleKeyGestureEvent(event);
+                }
+
+                @Override
+                public boolean isKeyGestureSupported(int gestureType) {
+                    return switch (gestureType) {
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
+                        default -> false;
+                    };
+                }
+            };
+
     @VisibleForTesting
     AccessibilityManagerService(
             Context context,
@@ -542,6 +568,7 @@
         mUmi = LocalServices.getService(UserManagerInternal.class);
         // TODO(b/255426725): not used on tests
         mVisibleBgUserIds = null;
+        mInputManager = context.getSystemService(InputManager.class);
 
         init();
     }
@@ -583,6 +610,7 @@
                 mUiAutomationManager, this);
         mFlashNotificationsController = new FlashNotificationsController(mContext);
         mUmi = LocalServices.getService(UserManagerInternal.class);
+        mInputManager = context.getSystemService(InputManager.class);
 
         if (UserManager.isVisibleBackgroundUsersEnabled()) {
             mVisibleBgUserIds = new SparseBooleanArray();
@@ -599,6 +627,9 @@
         registerBroadcastReceivers();
         new AccessibilityContentObserver(mMainHandler).register(
                 mContext.getContentResolver());
+        if (enableTalkbackAndMagnifierKeyGestures()) {
+            mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
+        }
         disableAccessibilityMenuToMigrateIfNeeded();
     }
 
@@ -640,6 +671,79 @@
         return mIsAccessibilityButtonShown;
     }
 
+    @VisibleForTesting
+    boolean handleKeyGestureEvent(KeyGestureEvent event) {
+        final boolean complete =
+                event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                        && !event.isCancelled();
+        final int gestureType = event.getKeyGestureType();
+        if (!complete) {
+            return false;
+        }
+
+        String targetName;
+        switch (gestureType) {
+            case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
+                targetName = MAGNIFICATION_CONTROLLER_NAME;
+                break;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
+                targetName = mContext.getString(R.string.config_defaultSelectToSpeakService);
+                if (targetName.isEmpty()) {
+                    return false;
+                }
+
+                final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName)
+                        ? null : ComponentName.unflattenFromString(targetName);
+                AccessibilityServiceInfo accessibilityServiceInfo;
+                synchronized (mLock) {
+                    AccessibilityUserState userState = getCurrentUserStateLocked();
+                    accessibilityServiceInfo =
+                            userState.getInstalledServiceInfoLocked(targetServiceComponent);
+                }
+                if (accessibilityServiceInfo == null) {
+                    return false;
+                }
+
+                // Skip enabling if a warning dialog is required for the feature.
+                // TODO(b/377752960): Explore better options to instead show the warning dialog
+                //  in this scenario.
+                if (isAccessibilityServiceWarningRequired(accessibilityServiceInfo)) {
+                    Slog.w(LOG_TAG,
+                            "Accessibility warning is required before this service can be "
+                                    + "activated automatically via KEY_GESTURE shortcut.");
+                    return false;
+                }
+                break;
+            default:
+                return false;
+        }
+
+        List<String> shortcutTargets = getAccessibilityShortcutTargets(
+                KEY_GESTURE);
+        if (!shortcutTargets.contains(targetName)) {
+            int userId;
+            synchronized (mLock) {
+                userId = mCurrentUserId;
+            }
+            // TODO(b/377752960): Add dialog to confirm enabling the service and to
+            //  activate the first time.
+            enableShortcutForTargets(true, UserShortcutType.KEY_GESTURE,
+                    List.of(targetName), userId);
+
+            // Do not perform action on first press since it was just registered. Eventually,
+            // this will be a separate dialog that appears that requires the user to confirm
+            // which will resolve this race condition. For now, just require two presses the
+            // first time it is activated.
+            return true;
+        }
+
+        final int displayId = event.getDisplayId() != INVALID_DISPLAY
+                ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId();
+        performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName);
+
+        return true;
+    }
+
     @Override
     public Pair<float[], MagnificationSpec> getWindowTransformationMatrixAndMagnificationSpec(
             int windowId) {
@@ -1224,14 +1328,14 @@
             int displayId = event.getDisplayId();
             final int windowId = event.getWindowId();
             if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
-                    && displayId == Display.INVALID_DISPLAY) {
+                    && displayId == INVALID_DISPLAY) {
                 displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId(
                         resolvedUserId, windowId);
                 event.setDisplayId(displayId);
             }
             synchronized (mLock) {
                 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
-                        && displayId != Display.INVALID_DISPLAY
+                        && displayId != INVALID_DISPLAY
                         && mA11yWindowManager.isTrackingWindowsLocked(displayId)) {
                     shouldComputeWindows = true;
                 }
@@ -3257,6 +3361,7 @@
         updateAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         updateAccessibilityShortcutTargetsLocked(userState, GESTURE);
         updateAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
+        updateAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
         // Update the capabilities before the mode because we will check the current mode is
         // invalid or not..
         updateMagnificationCapabilitiesSettingsChangeLocked(userState);
@@ -3387,6 +3492,7 @@
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, GESTURE);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
         somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
         somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
         somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3968,6 +4074,7 @@
         if (android.provider.Flags.a11yStandaloneGestureEnabled()) {
             shortcutTypes.add(GESTURE);
         }
+        shortcutTypes.add(KEY_GESTURE);
 
         final ComponentName serviceName = service.getComponentName();
         for (Integer shortcutType: shortcutTypes) {
@@ -4078,13 +4185,15 @@
      */
     private void performAccessibilityShortcutInternal(int displayId,
             @UserShortcutType int shortcutType, @Nullable String targetName) {
-        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType);
+        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(
+                shortcutType);
         if (shortcutTargets.isEmpty()) {
             Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType);
             return;
         }
         // In case the caller specified a target name
-        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, targetName)) {
+        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets,
+                targetName)) {
             Slog.v(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName);
             targetName = null;
         }
@@ -4306,6 +4415,13 @@
             return;
         }
 
+        if (shortcutType == UserShortcutType.KEY_GESTURE
+                && !enableTalkbackAndMagnifierKeyGestures()) {
+            Slog.w(LOG_TAG,
+                    "KEY_GESTURE type shortcuts are disabled by feature flag");
+            return;
+        }
+
         final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType);
         if (shortcutType == UserShortcutType.TRIPLETAP
                 || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) {
@@ -5071,6 +5187,11 @@
     @EnforcePermission(MANAGE_ACCESSIBILITY)
     public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) {
         isAccessibilityServiceWarningRequired_enforcePermission();
+        if (info == null) {
+            Log.e(LOG_TAG, "Called isAccessibilityServiceWarningRequired with null service info");
+            return true;
+        }
+
         final ComponentName componentName = info.getComponentName();
 
         // Warning is not required if the service is already enabled.
@@ -5678,6 +5799,9 @@
         private final Uri mAccessibilityGestureTargetsUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS);
 
+        private final Uri mAccessibilityKeyGestureTargetsUri = Settings.Secure.getUriFor(
+                Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS);
+
         private final Uri mUserNonInteractiveUiTimeoutUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS);
 
@@ -5742,6 +5866,8 @@
             contentResolver.registerContentObserver(
                     mAccessibilityGestureTargetsUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
+                    mAccessibilityKeyGestureTargetsUri, false, this, UserHandle.USER_ALL);
+            contentResolver.registerContentObserver(
                     mUserNonInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
                     mUserInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL);
@@ -5823,6 +5949,10 @@
                     if (readAccessibilityShortcutTargetsLocked(userState, GESTURE)) {
                         onUserStateChangedLocked(userState);
                     }
+                } else if (mAccessibilityKeyGestureTargetsUri.equals(uri)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE)) {
+                        onUserStateChangedLocked(userState);
+                    }
                 } else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
                         || mUserInteractiveUiTimeoutUri.equals(uri)) {
                     readUserRecommendedUiTimeoutSettingsLocked(userState);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 67b4063..8b3e63d 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -29,6 +29,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -209,6 +210,7 @@
         mShortcutTargets.put(SOFTWARE, new ArraySet<>());
         mShortcutTargets.put(GESTURE, new ArraySet<>());
         mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>());
+        mShortcutTargets.put(KEY_GESTURE, new ArraySet<>());
     }
 
     boolean isHandlingAccessibilityEventsLocked() {
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index d40e747..51c4305 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -27,6 +27,7 @@
 import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID;
 
 import android.accessibilityservice.MagnificationConfig;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -101,6 +102,7 @@
     private int mMagnificationCapabilities = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
     /** Whether the platform supports window magnification feature. */
     private final boolean mSupportWindowMagnification;
+    private final MagnificationScaleStepProvider mScaleStepProvider;
 
     private final Executor mBackgroundExecutor;
 
@@ -131,6 +133,14 @@
             .UiChangesForAccessibilityCallbacks> mAccessibilityCallbacksDelegateArray =
             new SparseArray<>();
 
+    // Direction magnifier scale can be altered.
+    public static final int ZOOM_DIRECTION_IN = 0;
+    public static final int ZOOM_DIRECTION_OUT = 1;
+
+    @IntDef({ZOOM_DIRECTION_IN, ZOOM_DIRECTION_OUT})
+    public @interface ZoomDirection {
+    }
+
     /**
      * A callback to inform the magnification transition result on the given display.
      */
@@ -144,6 +154,41 @@
         void onResult(int displayId, boolean success);
     }
 
+
+    /**
+     * An interface to configure how much the magnification scale should be affected when moving in
+     * steps.
+     */
+    public interface MagnificationScaleStepProvider {
+        /**
+         * Calculate the next value given which direction (in/out) to adjust the magnification
+         * scale.
+         *
+         * @param currentScale The current magnification scale value.
+         * @param direction    Whether to zoom in or out.
+         * @return The next scale value.
+         */
+        float nextScaleStep(float currentScale, @ZoomDirection int direction);
+    }
+
+    public static class DefaultMagnificationScaleStepProvider implements
+            MagnificationScaleStepProvider {
+        // Factor of magnification scale. For example, when this value is 1.189, scale
+        // value will be changed x1.000, x1.189, x1.414, x1.681, x2.000, ...
+        // Note: this value is 2.0 ^ (1 / 4).
+        public static final float ZOOM_STEP_SCALE_FACTOR = 1.18920712f;
+
+        @Override
+        public float nextScaleStep(float currentScale, @ZoomDirection int direction) {
+            final int stepDelta = direction == ZOOM_DIRECTION_IN ? 1 : -1;
+            final long scaleIndex = Math.round(
+                    Math.log(currentScale) / Math.log(ZOOM_STEP_SCALE_FACTOR));
+            final float nextScale = (float) Math.pow(ZOOM_STEP_SCALE_FACTOR,
+                    scaleIndex + stepDelta);
+            return MagnificationScaleProvider.constrainScale(nextScale);
+        }
+    }
+
     public MagnificationController(AccessibilityManagerService ams, Object lock,
             Context context, MagnificationScaleProvider scaleProvider,
             Executor backgroundExecutor) {
@@ -156,6 +201,7 @@
                 .getAccessibilityController().setUiChangesForAccessibilityCallbacks(this);
         mSupportWindowMagnification = context.getPackageManager().hasSystemFeature(
                 FEATURE_WINDOW_MAGNIFICATION);
+        mScaleStepProvider = new DefaultMagnificationScaleStepProvider();
 
         mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag(context);
         mAlwaysOnMagnificationFeatureFlag.addOnChangedListener(
@@ -891,6 +937,37 @@
         return isActivated;
     }
 
+    /**
+     * Scales the magnifier on the given display one step in/out based on the zoomIn param.
+     *
+     * @param displayId The logical display id.
+     * @param direction Whether the scale should be zoomed in or out.
+     * @return {@code true} if the magnification scale was affected.
+     */
+    public boolean scaleMagnificationByStep(int displayId, @ZoomDirection int direction) {
+        if (getFullScreenMagnificationController().isActivated(displayId)) {
+            final float magnificationScale = getFullScreenMagnificationController().getScale(
+                    displayId);
+            final float nextMagnificationScale = mScaleStepProvider.nextScaleStep(
+                    magnificationScale, direction);
+            getFullScreenMagnificationController().setScaleAndCenter(displayId,
+                    nextMagnificationScale,
+                    Float.NaN, Float.NaN, true, MAGNIFICATION_GESTURE_HANDLER_ID);
+            return nextMagnificationScale != magnificationScale;
+        }
+
+        if (getMagnificationConnectionManager().isWindowMagnifierEnabled(displayId)) {
+            final float magnificationScale = getMagnificationConnectionManager().getScale(
+                    displayId);
+            final float nextMagnificationScale = mScaleStepProvider.nextScaleStep(
+                    magnificationScale, direction);
+            getMagnificationConnectionManager().setScale(displayId, nextMagnificationScale);
+            return nextMagnificationScale != magnificationScale;
+        }
+
+        return false;
+    }
+
     private final class DisableMagnificationCallback implements
             MagnificationAnimationCallback {
         private final TransitionCallBack mTransitionCallBack;
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 268e564..f13e229 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -29,7 +29,7 @@
 import android.app.appfunctions.AppFunctionRuntimeMetadata;
 import android.app.appfunctions.AppFunctionStaticMetadataHelper;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
-import android.app.appfunctions.ExecuteAppFunctionResponse;
+import android.app.appfunctions.AppFunctionException;
 import android.app.appfunctions.IAppFunctionEnabledCallback;
 import android.app.appfunctions.IAppFunctionManager;
 import android.app.appfunctions.IAppFunctionService;
@@ -156,11 +156,10 @@
             mCallerValidator.verifyTargetUserHandle(
                     requestInternal.getUserHandle(), validatedCallingPackage);
         } catch (SecurityException exception) {
-            safeExecuteAppFunctionCallback.onResult(
-                    ExecuteAppFunctionResponse.newFailure(
-                            ExecuteAppFunctionResponse.RESULT_DENIED,
-                            exception.getMessage(),
-                            /* extras= */ null));
+            safeExecuteAppFunctionCallback.onError(
+                    new AppFunctionException(
+                            AppFunctionException.ERROR_DENIED,
+                            exception.getMessage()));
             return null;
         }
 
@@ -180,7 +179,7 @@
                                 safeExecuteAppFunctionCallback,
                                 executeAppFunctionCallback.asBinder());
                     } catch (Exception e) {
-                        safeExecuteAppFunctionCallback.onResult(
+                        safeExecuteAppFunctionCallback.onError(
                                 mapExceptionToExecuteAppFunctionResponse(e));
                     }
                 });
@@ -198,22 +197,19 @@
         UserHandle targetUser = requestInternal.getUserHandle();
         // TODO(b/354956319): Add and honor the new enterprise policies.
         if (mCallerValidator.isUserOrganizationManaged(targetUser)) {
-            safeExecuteAppFunctionCallback.onResult(
-                    ExecuteAppFunctionResponse.newFailure(
-                            ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR,
+            safeExecuteAppFunctionCallback.onError(
+                    new AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR,
                             "Cannot run on a device with a device owner or from the managed"
-                                    + " profile.",
-                            /* extras= */ null));
+                                    + " profile."));
             return;
         }
 
         String targetPackageName = requestInternal.getClientRequest().getTargetPackageName();
         if (TextUtils.isEmpty(targetPackageName)) {
-            safeExecuteAppFunctionCallback.onResult(
-                    ExecuteAppFunctionResponse.newFailure(
-                            ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT,
-                            "Target package name cannot be empty.",
-                            /* extras= */ null));
+            safeExecuteAppFunctionCallback.onError(
+                    new AppFunctionException(
+                            AppFunctionException.ERROR_INVALID_ARGUMENT,
+                            "Target package name cannot be empty."));
             return;
         }
 
@@ -253,11 +249,10 @@
                                     mInternalServiceHelper.resolveAppFunctionService(
                                             targetPackageName, targetUser);
                             if (serviceIntent == null) {
-                                safeExecuteAppFunctionCallback.onResult(
-                                        ExecuteAppFunctionResponse.newFailure(
-                                                ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR,
-                                                "Cannot find the target service.",
-                                                /* extras= */ null));
+                                safeExecuteAppFunctionCallback.onError(
+                                        new AppFunctionException(
+                                                AppFunctionException.ERROR_SYSTEM_ERROR,
+                                                "Cannot find the target service."));
                                 return;
                             }
                             bindAppFunctionServiceUnchecked(
@@ -272,7 +267,7 @@
                         })
                 .exceptionally(
                         ex -> {
-                            safeExecuteAppFunctionCallback.onResult(
+                            safeExecuteAppFunctionCallback.onError(
                                     mapExceptionToExecuteAppFunctionResponse(ex));
                             return null;
                         });
@@ -446,11 +441,9 @@
 
         if (!bindServiceResult) {
             Slog.e(TAG, "Failed to bind to the AppFunctionService");
-            safeExecuteAppFunctionCallback.onResult(
-                    ExecuteAppFunctionResponse.newFailure(
-                            ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR,
-                            "Failed to bind the AppFunctionService.",
-                            /* extras= */ null));
+            safeExecuteAppFunctionCallback.onError(
+                    new AppFunctionException(AppFunctionException.ERROR_SYSTEM_ERROR,
+                            "Failed to bind the AppFunctionService."));
         }
     }
 
@@ -459,22 +452,21 @@
                 .getSystemService(AppSearchManager.class);
     }
 
-    private ExecuteAppFunctionResponse mapExceptionToExecuteAppFunctionResponse(Throwable e) {
+    private AppFunctionException mapExceptionToExecuteAppFunctionResponse(Throwable e) {
         if (e instanceof CompletionException) {
             e = e.getCause();
         }
-        int resultCode = ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR;
+        int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR;
         if (e instanceof AppSearchException appSearchException) {
             resultCode =
                     mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(
                             appSearchException.getResultCode());
         } else if (e instanceof SecurityException) {
-            resultCode = ExecuteAppFunctionResponse.RESULT_DENIED;
+            resultCode = AppFunctionException.ERROR_DENIED;
         } else if (e instanceof DisabledAppFunctionException) {
-            resultCode = ExecuteAppFunctionResponse.RESULT_DISABLED;
+            resultCode = AppFunctionException.ERROR_DISABLED;
         }
-        return ExecuteAppFunctionResponse.newFailure(
-                resultCode, e.getMessage(), /* extras= */ null);
+        return new AppFunctionException(resultCode, e.getMessage());
     }
 
     private int mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(int resultCode) {
@@ -485,13 +477,13 @@
 
         switch (resultCode) {
             case AppSearchResult.RESULT_NOT_FOUND:
-                return ExecuteAppFunctionResponse.RESULT_FUNCTION_NOT_FOUND;
+                return AppFunctionException.ERROR_FUNCTION_NOT_FOUND;
             case AppSearchResult.RESULT_INVALID_ARGUMENT:
             case AppSearchResult.RESULT_INTERNAL_ERROR:
             case AppSearchResult.RESULT_SECURITY_ERROR:
                 // fall-through
         }
-        return ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR;
+        return AppFunctionException.ERROR_SYSTEM_ERROR;
     }
 
     private void registerAppSearchObserver(@NonNull TargetUser user) {
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
index 129be65..c689bb9 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
@@ -17,6 +17,7 @@
 
 import android.annotation.NonNull;
 import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
+import android.app.appfunctions.AppFunctionException;
 import android.app.appfunctions.ExecuteAppFunctionResponse;
 import android.app.appfunctions.IAppFunctionService;
 import android.app.appfunctions.ICancellationCallback;
@@ -57,17 +58,22 @@
                     mCancellationCallback,
                     new IExecuteAppFunctionCallback.Stub() {
                         @Override
-                        public void onResult(ExecuteAppFunctionResponse response) {
+                        public void onSuccess(ExecuteAppFunctionResponse response) {
                             mSafeExecuteAppFunctionCallback.onResult(response);
                             serviceUsageCompleteListener.onCompleted();
                         }
+
+                        @Override
+                        public void onError(AppFunctionException error) {
+                            mSafeExecuteAppFunctionCallback.onError(error);
+                            serviceUsageCompleteListener.onCompleted();
+                        }
                     });
         } catch (Exception e) {
-            mSafeExecuteAppFunctionCallback.onResult(
-                    ExecuteAppFunctionResponse.newFailure(
-                            ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR,
-                            e.getMessage(),
-                            /* extras= */ null));
+            mSafeExecuteAppFunctionCallback.onError(
+                    new AppFunctionException(
+                            AppFunctionException.ERROR_APP_UNKNOWN_ERROR,
+                            e.getMessage()));
             serviceUsageCompleteListener.onCompleted();
         }
     }
@@ -75,11 +81,9 @@
     @Override
     public void onFailedToConnect() {
         Slog.e(TAG, "Failed to connect to service");
-        mSafeExecuteAppFunctionCallback.onResult(
-                ExecuteAppFunctionResponse.newFailure(
-                        ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR,
-                        "Failed to connect to AppFunctionService",
-                        /* extras= */ null));
+        mSafeExecuteAppFunctionCallback.onError(
+                new AppFunctionException(AppFunctionException.ERROR_APP_UNKNOWN_ERROR,
+                        "Failed to connect to AppFunctionService"));
     }
 
     @Override
diff --git a/services/art-profile b/services/art-profile
index 6fa4c88..ce1e2c6 100644
--- a/services/art-profile
+++ b/services/art-profile
@@ -5657,7 +5657,7 @@
 Lcom/android/server/utils/Watcher;
 Lcom/android/server/vibrator/VibratorController$NativeWrapper;
 Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener;
-Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;
+Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;
 Lcom/android/server/vibrator/VibratorManagerService;
 Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener;
 Lcom/android/server/vr/VrManagerService;
diff --git a/services/art-wear-profile b/services/art-wear-profile
index 47bdb13..1e3090f 100644
--- a/services/art-wear-profile
+++ b/services/art-wear-profile
@@ -1330,7 +1330,7 @@
 Lcom/android/server/utils/Watcher;
 Lcom/android/server/vibrator/VibratorController$NativeWrapper;
 Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener;
-Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;
+Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;
 Lcom/android/server/vibrator/VibratorManagerService;
 Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener;
 Lcom/android/server/vr/VrManagerService;
@@ -24948,7 +24948,7 @@
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->cancelSynced()V
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getCapabilities()J
 PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getVibratorIds()[I
-PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)V
+PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;-><init>(Lcom/android/server/vibrator/VibratorManagerService;)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;->onComplete(IJ)V
 PLcom/android/server/vibrator/VibratorManagerService$VibrationRecords;-><init>(II)V
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index ddccb37..466d477 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -281,6 +281,9 @@
     private static final int SCHEDULE_FILE_VERSION = 1;
 
     public static final String SETTINGS_PACKAGE = "com.android.providers.settings";
+
+    public static final String TELEPHONY_PROVIDER_PACKAGE = "com.android.providers.telephony";
+
     public static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup";
 
     // Pseudoname that we use for the Package Manager metadata "package".
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index f24a3c1..508b62c 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -21,6 +21,7 @@
 import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL;
 import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE;
 import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
+import static com.android.server.backup.UserBackupManagerService.TELEPHONY_PROVIDER_PACKAGE;
 import static com.android.server.backup.UserBackupManagerService.WALLPAPER_PACKAGE;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 
@@ -75,6 +76,12 @@
             systemPackagesAllowedForProfileUser,
             Sets.newArraySet(WALLPAPER_PACKAGE, SETTINGS_PACKAGE));
 
+    static {
+        if (UserManager.isHeadlessSystemUserMode()) {
+            systemPackagesAllowedForNonSystemUsers.add(TELEPHONY_PROVIDER_PACKAGE);
+        }
+    }
+
     private final PackageManager mPackageManager;
     private final PackageManagerInternal mPackageManagerInternal;
     private final int mUserId;
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3ccad160..aea16b0 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -158,6 +158,7 @@
         "android.hardware.gnss-V2-java",
         "android.hardware.vibrator-V3-java",
         "app-compat-annotations",
+        "art_exported_aconfig_flags_lib",
         "framework-tethering.stubs.module_lib",
         "keepanno-annotations",
         "service-art.stubs.system_server",
@@ -237,6 +238,7 @@
         "connectivity_flags_lib",
         "device_config_service_flags_java",
         "dreams_flags_lib",
+        "aconfig_flags_java",
         "aconfig_new_storage_flags_lib",
         "powerstats_flags_lib",
         "locksettings_flags_lib",
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 72a9a2d..fa22862 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -88,6 +88,7 @@
 import android.telephony.ims.ImsCallSession;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.MediaQualityStatus;
+import android.telephony.satellite.NtnSignalStrength;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -440,6 +441,7 @@
     private boolean[] mCarrierRoamingNtnEligible = null;
 
     private List<IntArray> mCarrierRoamingNtnAvailableServices;
+    private NtnSignalStrength[] mCarrierRoamingNtnSignalStrength;
 
     // Local cache to check if Satellite Modem is enabled
     private AtomicBoolean mIsSatelliteEnabled;
@@ -745,6 +747,12 @@
             mSCBMDuration = copyOf(mSCBMDuration, mNumPhones);
             mCarrierRoamingNtnMode = copyOf(mCarrierRoamingNtnMode, mNumPhones);
             mCarrierRoamingNtnEligible = copyOf(mCarrierRoamingNtnEligible, mNumPhones);
+            if (mCarrierRoamingNtnSignalStrength != null) {
+                mCarrierRoamingNtnSignalStrength = copyOf(
+                        mCarrierRoamingNtnSignalStrength, mNumPhones);
+            } else {
+                mCarrierRoamingNtnSignalStrength = new NtnSignalStrength[mNumPhones];
+            }
             // ds -> ss switch.
             if (mNumPhones < oldNumPhones) {
                 cutListToSize(mCellInfo, mNumPhones);
@@ -807,6 +815,8 @@
                 mCarrierRoamingNtnMode[i] = false;
                 mCarrierRoamingNtnEligible[i] = false;
                 mCarrierRoamingNtnAvailableServices.add(i, new IntArray());
+                mCarrierRoamingNtnSignalStrength[i] = new NtnSignalStrength(
+                        NtnSignalStrength.NTN_SIGNAL_STRENGTH_NONE);
             }
         }
     }
@@ -883,6 +893,7 @@
         mCarrierRoamingNtnMode = new boolean[numPhones];
         mCarrierRoamingNtnEligible = new boolean[numPhones];
         mCarrierRoamingNtnAvailableServices = new ArrayList<>();
+        mCarrierRoamingNtnSignalStrength = new NtnSignalStrength[numPhones];
         mIsSatelliteEnabled = new AtomicBoolean();
         mWasSatelliteEnabledNotified = new AtomicBoolean();
 
@@ -932,6 +943,8 @@
             mCarrierRoamingNtnMode[i] = false;
             mCarrierRoamingNtnEligible[i] = false;
             mCarrierRoamingNtnAvailableServices.add(i, new IntArray());
+            mCarrierRoamingNtnSignalStrength[i] = new NtnSignalStrength(
+                    NtnSignalStrength.NTN_SIGNAL_STRENGTH_NONE);
         }
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1565,6 +1578,15 @@
                         remove(r.binder);
                     }
                 }
+                if (events.contains(
+                        TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnSignalStrengthChanged(
+                                mCarrierRoamingNtnSignalStrength[r.phoneId]);
+                    } catch (RemoteException ex) {
+                        remove(r.binder);
+                    }
+                }
             }
         }
     }
@@ -3803,6 +3825,44 @@
         }
     }
 
+
+    /**
+     * Notify external listeners that carrier roaming non-terrestrial network
+     * signal strength changed.
+     * @param subId subscription ID.
+     * @param ntnSignalStrength non-terrestrial network signal strength.
+     */
+    public void notifyCarrierRoamingNtnSignalStrengthChanged(int subId,
+            @NonNull NtnSignalStrength ntnSignalStrength) {
+        if (!checkNotifyPermission("notifyCarrierRoamingNtnSignalStrengthChanged")) {
+            log("nnotifyCarrierRoamingNtnSignalStrengthChanged: caller does not have required "
+                    + "permissions.");
+            return;
+        }
+
+        if (VDBG) {
+            log("notifyCarrierRoamingNtnSignalStrengthChanged: "
+                    + "subId=" + subId + " ntnSignalStrength=" + ntnSignalStrength.getLevel());
+        }
+
+        synchronized (mRecords) {
+            int phoneId = getPhoneIdFromSubId(subId);
+            mCarrierRoamingNtnSignalStrength[phoneId] = ntnSignalStrength;
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_SIGNAL_STRENGTH_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        r.callback.onCarrierRoamingNtnSignalStrengthChanged(ntnSignalStrength);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
+                    }
+                }
+            }
+            handleRemoveListLocked();
+        }
+    }
+
     @NeverCompile // Avoid size overhead of debugging code.
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
@@ -3858,6 +3918,8 @@
                 pw.println("mSCBMDuration=" + mSCBMDuration[i]);
                 pw.println("mCarrierRoamingNtnMode=" + mCarrierRoamingNtnMode[i]);
                 pw.println("mCarrierRoamingNtnEligible=" + mCarrierRoamingNtnEligible[i]);
+                pw.println("mCarrierRoamingNtnSignalStrength="
+                        + mCarrierRoamingNtnSignalStrength[i]);
 
                 // We need to obfuscate package names, and primitive arrays' native toString is ugly
                 Pair<List<String>, int[]> carrierPrivilegeState = mCarrierPrivilegeStates.get(i);
diff --git a/services/core/java/com/android/server/adaptiveauth/OWNERS b/services/core/java/com/android/server/adaptiveauth/OWNERS
deleted file mode 100644
index b188105..0000000
--- a/services/core/java/com/android/server/adaptiveauth/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-hainingc@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b5dcdb1..0826c53 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1101,7 +1101,7 @@
                 ProfilingServiceHelper.getInstance().onProfilingTriggerOccurred(
                         startInfo.getRealUid(),
                         startInfo.getPackageName(),
-                        ProfilingTrigger.TRIGGER_TYPE_APP_COLD_START_ACTIVITY);
+                        ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN);
             }
         }
     };
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index aa9ac6c..2eb9f3c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -124,6 +124,7 @@
 import com.android.server.Watchdog;
 import com.android.server.net.BaseNetworkObserver;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.power.feature.PowerManagerFlags;
 import com.android.server.power.optimization.Flags;
 import com.android.server.power.stats.BatteryExternalStatsWorker;
 import com.android.server.power.stats.BatteryStatsDumpHelperImpl;
@@ -195,6 +196,7 @@
     private final BatteryStats.BatteryStatsDumpHelper mDumpHelper;
     private final PowerStatsUidResolver mPowerStatsUidResolver = new PowerStatsUidResolver();
     private final PowerAttributor mPowerAttributor;
+    private final PowerManagerFlags mPowerManagerFlags = new PowerManagerFlags();
 
     private volatile boolean mMonitorEnabled = true;
     private boolean mRailsStatsCollectionEnabled = true;
@@ -617,6 +619,9 @@
                 BatteryConsumer.POWER_COMPONENT_ANY,
                 Flags.streamlinedMiscBatteryStats());
 
+        mStats.setMoveWscLoggingToNotifierEnabled(
+                mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled());
+
         mWorker.systemServicesReady();
         mStats.systemServicesReady(mContext);
         mCpuWakeupStats.systemServicesReady();
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index b0f88071..8a12858 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -593,7 +593,7 @@
                             originalStickyCallingUid, BackgroundStartPrivileges.NONE,
                             false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
                             null /* filterExtrasForReceiver */,
-                            broadcast.originalCallingAppProcessState);
+                            broadcast.originalCallingAppProcessState, mService.mPlatformCompat);
                     queue.enqueueBroadcastLocked(r);
                 }
             }
@@ -1631,7 +1631,7 @@
                     receivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
                     ordered, sticky, false, userId,
                     backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver,
-                    callerAppProcessState);
+                    callerAppProcessState, mService.mPlatformCompat);
             broadcastSentEventRecord.setBroadcastRecord(r);
 
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index f908c67..116aeea 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -42,6 +42,9 @@
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
 import android.app.BroadcastOptions.DeliveryGroupPolicy;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.compat.annotation.Overridable;
 import android.content.ComponentName;
 import android.content.IIntentReceiver;
 import android.content.Intent;
@@ -55,10 +58,12 @@
 import android.util.ArrayMap;
 import android.util.IntArray;
 import android.util.PrintWriterPrinter;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.compat.PlatformCompat;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -77,6 +82,16 @@
  * An active intent broadcast.
  */
 final class BroadcastRecord extends Binder {
+    /**
+     * Limit the scope of the priority values to the process level. This means that priority values
+     * will only influence the order of broadcast delivery within the same process.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
+    @Overridable
+    @VisibleForTesting
+    static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L;
+
     final @NonNull Intent intent;    // the original intent that generated us
     final @Nullable ComponentName targetComp; // original component name set on the intent
     final @Nullable ProcessRecord callerApp; // process that sent this
@@ -417,13 +432,13 @@
             @NonNull BackgroundStartPrivileges backgroundStartPrivileges,
             boolean timeoutExempt,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
-            int callerAppProcessState) {
+            int callerAppProcessState, PlatformCompat platformCompat) {
         this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid,
                 callingUid, callerInstantApp, resolvedType, requiredPermissions,
                 excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp,
                 resultTo, resultCode, resultData, resultExtras, serialized, sticky,
                 initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt,
-                filterExtrasForReceiver, callerAppProcessState);
+                filterExtrasForReceiver, callerAppProcessState, platformCompat);
     }
 
     BroadcastRecord(BroadcastQueue _queue,
@@ -439,7 +454,7 @@
             @NonNull BackgroundStartPrivileges backgroundStartPrivileges,
             boolean timeoutExempt,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
-            int callerAppProcessState) {
+            int callerAppProcessState, PlatformCompat platformCompat) {
         if (_intent == null) {
             throw new NullPointerException("Can't construct with a null intent");
         }
@@ -466,7 +481,8 @@
         urgent = calculateUrgent(_intent, _options);
         deferUntilActive = calculateDeferUntilActive(_callingUid,
                 _options, _resultTo, _serialized, urgent);
-        blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(receivers, _serialized);
+        blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(
+                receivers, _serialized, platformCompat);
         scheduledTime = new long[delivery.length];
         terminalTime = new long[delivery.length];
         resultToApp = _resultToApp;
@@ -730,7 +746,8 @@
     }
 
     /**
-     * Determine if the result of {@link #calculateBlockedUntilBeyondCount(List, boolean)}
+     * Determine if the result of
+     * {@link #calculateBlockedUntilBeyondCount(List, boolean, PlatformCompat)}
      * has prioritized tranches of receivers.
      */
     @VisibleForTesting
@@ -754,37 +771,121 @@
      */
     @VisibleForTesting
     static @NonNull int[] calculateBlockedUntilBeyondCount(
-            @NonNull List<Object> receivers, boolean ordered) {
+            @NonNull List<Object> receivers, boolean ordered, PlatformCompat platformCompat) {
         final int N = receivers.size();
         final int[] blockedUntilBeyondCount = new int[N];
-        int lastPriority = 0;
-        int lastPriorityIndex = 0;
-        for (int i = 0; i < N; i++) {
-            if (ordered) {
-                // When sending an ordered broadcast, we need to block this
-                // receiver until all previous receivers have terminated
+        if (ordered) {
+            // When sending an ordered broadcast, we need to block this
+            // receiver until all previous receivers have terminated
+            for (int i = 0; i < N; i++) {
                 blockedUntilBeyondCount[i] = i;
+            }
+        } else {
+            if (Flags.limitPriorityScope()) {
+                final boolean[] changeEnabled = calculateChangeStateForReceivers(
+                        receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat);
+
+                // Priority of the previous tranche
+                int lastTranchePriority = 0;
+                // Priority of the current tranche
+                int currentTranchePriority = 0;
+                // Index of the last receiver in the previous tranche
+                int lastTranchePriorityIndex = -1;
+                // Index of the last receiver with change disabled in the previous tranche
+                int lastTrancheChangeDisabledIndex = -1;
+                // Index of the last receiver with change disabled in the current tranche
+                int currentTrancheChangeDisabledIndex = -1;
+
+                for (int i = 0; i < N; i++) {
+                    final int thisPriority = getReceiverPriority(receivers.get(i));
+                    if (i == 0) {
+                        currentTranchePriority = thisPriority;
+                        if (!changeEnabled[i]) {
+                            currentTrancheChangeDisabledIndex = i;
+                        }
+                        continue;
+                    }
+
+                    // Check if a new priority tranche has started
+                    if (thisPriority != currentTranchePriority) {
+                        // Update tranche boundaries and reset the disabled index.
+                        if (currentTrancheChangeDisabledIndex != -1) {
+                            lastTrancheChangeDisabledIndex = currentTrancheChangeDisabledIndex;
+                        }
+                        lastTranchePriority = currentTranchePriority;
+                        lastTranchePriorityIndex = i - 1;
+                        currentTranchePriority = thisPriority;
+                        currentTrancheChangeDisabledIndex = -1;
+                    }
+                    if (!changeEnabled[i]) {
+                        currentTrancheChangeDisabledIndex = i;
+
+                        // Since the change is disabled, block the current receiver until the
+                        // last receiver in the previous tranche.
+                        blockedUntilBeyondCount[i] = lastTranchePriorityIndex + 1;
+                    } else if (thisPriority != lastTranchePriority) {
+                        // If the changeId was disabled for an earlier receiver and the current
+                        // receiver has a different priority, block the current receiver
+                        // until that earlier receiver.
+                        if (lastTrancheChangeDisabledIndex != -1) {
+                            blockedUntilBeyondCount[i] = lastTrancheChangeDisabledIndex + 1;
+                        }
+                    }
+                }
+                // If the entire list is in the same priority tranche or no receivers had
+                // changeId disabled, mark as -1 to indicate that none of them need to wait
+                if (N > 0 && (lastTranchePriorityIndex == -1
+                        || (lastTrancheChangeDisabledIndex == -1
+                                && currentTrancheChangeDisabledIndex == -1))) {
+                    Arrays.fill(blockedUntilBeyondCount, -1);
+                }
             } else {
                 // When sending a prioritized broadcast, we only need to wait
                 // for the previous tranche of receivers to be terminated
-                final int thisPriority = getReceiverPriority(receivers.get(i));
-                if ((i == 0) || (thisPriority != lastPriority)) {
-                    lastPriority = thisPriority;
-                    lastPriorityIndex = i;
-                    blockedUntilBeyondCount[i] = i;
-                } else {
-                    blockedUntilBeyondCount[i] = lastPriorityIndex;
+                int lastPriority = 0;
+                int lastPriorityIndex = 0;
+                for (int i = 0; i < N; i++) {
+                    final int thisPriority = getReceiverPriority(receivers.get(i));
+                    if ((i == 0) || (thisPriority != lastPriority)) {
+                        lastPriority = thisPriority;
+                        lastPriorityIndex = i;
+                        blockedUntilBeyondCount[i] = i;
+                    } else {
+                        blockedUntilBeyondCount[i] = lastPriorityIndex;
+                    }
+                }
+                // If the entire list is in the same priority tranche, mark as -1 to
+                // indicate that none of them need to wait
+                if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) {
+                    Arrays.fill(blockedUntilBeyondCount, -1);
                 }
             }
         }
-        // If the entire list is in the same priority tranche, mark as -1 to
-        // indicate that none of them need to wait
-        if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) {
-            Arrays.fill(blockedUntilBeyondCount, -1);
-        }
         return blockedUntilBeyondCount;
     }
 
+    @VisibleForTesting
+    static @NonNull boolean[] calculateChangeStateForReceivers(@NonNull List<Object> receivers,
+            long changeId, PlatformCompat platformCompat) {
+        final SparseBooleanArray changeStateForUids = new SparseBooleanArray();
+        final int count = receivers.size();
+        final boolean[] changeStateForReceivers = new boolean[count];
+        for (int i = 0; i < count; ++i) {
+            final int receiverUid = getReceiverUid(receivers.get(i));
+            final boolean isChangeEnabled;
+            final int idx = changeStateForUids.indexOfKey(receiverUid);
+            if (idx >= 0) {
+                isChangeEnabled = changeStateForUids.valueAt(idx);
+            } else {
+                isChangeEnabled = platformCompat.isChangeEnabledByUidInternalNoLogging(
+                        changeId, receiverUid);
+                changeStateForUids.put(receiverUid, isChangeEnabled);
+            }
+            changeStateForReceivers[i] = isChangeEnabled;
+        }
+        return changeStateForReceivers;
+    }
+
     static int getReceiverUid(@NonNull Object receiver) {
         if (receiver instanceof BroadcastFilter) {
             return ((BroadcastFilter) receiver).owningUid;
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 8dc7c73..3dd5ec9 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -42,6 +42,7 @@
 import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
 import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides;
 import static com.android.aconfig_new_storage.Flags.supportClearLocalOverridesImmediately;
+import static com.android.aconfig.flags.Flags.enableSystemAconfigdRust;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -144,7 +145,6 @@
         "android_core_networking",
         "android_health_services",
         "android_sdk",
-        "android_stylus",
         "aoc",
         "app_widgets",
         "arc_next",
@@ -209,6 +209,7 @@
         "pixel_continuity",
         "pixel_perf",
         "pixel_sensors",
+        "pixel_state_server",
         "pixel_system_sw_video",
         "pixel_video_sw",
         "pixel_watch",
@@ -456,9 +457,11 @@
     static ProtoInputStream sendAconfigdRequests(ProtoOutputStream requests) {
         // connect to aconfigd socket
         LocalSocket client = new LocalSocket();
+        String socketName = enableSystemAconfigdRust()
+                    ? "aconfigd_system" : "aconfigd";
         try{
             client.connect(new LocalSocketAddress(
-                "aconfigd", LocalSocketAddress.Namespace.RESERVED));
+                socketName, LocalSocketAddress.Namespace.RESERVED));
             Slog.d(TAG, "connected to aconfigd socket");
         } catch (IOException ioe) {
             logErr("failed to connect to aconfigd socket", ioe);
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index b1185d5..7f169db 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -7,4 +7,12 @@
     description: "Restrict priority values defined by non-system apps"
     is_fixed_read_only: true
     bug: "369487976"
+}
+
+flag {
+    name: "limit_priority_scope"
+    namespace: "backstage_power"
+    description: "Limit the scope of receiver priorities to within a process"
+    is_fixed_read_only: true
+    bug: "369487976"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 09de894..34d4fb0 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -40,6 +40,7 @@
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
+import android.media.AudioDescriptor;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioDevicePort;
@@ -47,6 +48,7 @@
 import android.media.AudioManager;
 import android.media.AudioManager.AudioDeviceCategory;
 import android.media.AudioPort;
+import android.media.AudioProfile;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
@@ -619,6 +621,8 @@
         final int mGroupId;
         @NonNull String mPeerDeviceAddress;
         @NonNull String mPeerIdentityDeviceAddress;
+        @NonNull List<AudioProfile> mAudioProfiles;
+        @NonNull List<AudioDescriptor> mAudioDescriptors;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
@@ -627,7 +631,8 @@
 
         DeviceInfo(int deviceType, String deviceName, String address,
                    String identityAddress, int codecFormat,
-                   int groupId, String peerAddress, String peerIdentityAddress) {
+                   int groupId, String peerAddress, String peerIdentityAddress,
+                   List<AudioProfile> profiles, List<AudioDescriptor> descriptors) {
             mDeviceType = deviceType;
             mDeviceName = TextUtils.emptyIfNull(deviceName);
             mDeviceAddress = TextUtils.emptyIfNull(address);
@@ -639,6 +644,16 @@
             mGroupId = groupId;
             mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress);
             mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress);
+            mAudioProfiles = profiles;
+            mAudioDescriptors = descriptors;
+        }
+
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat,
+                   int groupId, String peerAddress, String peerIdentityAddress) {
+            this(deviceType, deviceName, address, identityAddress, codecFormat,
+                    groupId, peerAddress, peerIdentityAddress,
+                    new ArrayList<>(), new ArrayList<>());
         }
 
         /** Constructor for all devices except A2DP sink and LE Audio */
@@ -646,6 +661,13 @@
             this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT);
         }
 
+        /** Constructor for HDMI OUT, HDMI ARC/EARC sink devices */
+        DeviceInfo(int deviceType, String deviceName, String address,
+            List<AudioProfile> profiles, List<AudioDescriptor> descriptors) {
+            this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT,
+                BluetoothLeAudio.GROUP_ID_INVALID, null, null, profiles, descriptors);
+        }
+
         /** Constructor for A2DP sink devices */
         DeviceInfo(int deviceType, String deviceName, String address,
                    String identityAddress, int codecFormat) {
@@ -1194,27 +1216,31 @@
     }
 
     /*package*/ void onToggleHdmi() {
-        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
-                .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI));
+        final int[] hdmiDevices = { AudioSystem.DEVICE_OUT_HDMI,
+                AudioSystem.DEVICE_OUT_HDMI_ARC, AudioSystem.DEVICE_OUT_HDMI_EARC };
+
         synchronized (mDevicesLock) {
-            // Is HDMI connected?
-            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, "");
-            final DeviceInfo di = mConnectedDevices.get(key);
-            if (di == null) {
-                Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi");
-                mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record();
-                return;
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                boolean isHdmiDevice = Arrays.stream(hdmiDevices).anyMatch(device ->
+                    device == di.mDeviceType);
+                if (isHdmiDevice) {
+                    MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
+                            .set(MediaMetrics.Property.DEVICE,
+                                    AudioSystem.getDeviceName(di.mDeviceType));
+                    AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                            AudioDeviceAttributes.ROLE_OUTPUT,
+                            AudioDeviceInfo.convertInternalDeviceToDeviceType(di.mDeviceType),
+                            di.mDeviceAddress, di.mDeviceName, di.mAudioProfiles,
+                            di.mAudioDescriptors);
+                    // Toggle HDMI to retrigger broadcast with proper formats.
+                    setWiredDeviceConnectionState(ada,
+                            AudioSystem.DEVICE_STATE_UNAVAILABLE, "onToggleHdmi"); // disconnect
+                    setWiredDeviceConnectionState(ada,
+                            AudioSystem.DEVICE_STATE_AVAILABLE, "onToggleHdmi"); // reconnect
+                    mmi.record();
+                }
             }
-            // Toggle HDMI to retrigger broadcast with proper formats.
-            setWiredDeviceConnectionState(
-                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
-                    AudioSystem.DEVICE_STATE_UNAVAILABLE, "android"); // disconnect
-            setWiredDeviceConnectionState(
-                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
-                    AudioSystem.DEVICE_STATE_AVAILABLE, "android"); // reconnect
         }
-        mmi.record();
     }
 
     @GuardedBy("mDevicesLock")
@@ -1818,7 +1844,15 @@
                             .printSlog(EventLogger.Event.ALOGE, TAG));
                     return false;
                 }
-                mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address));
+
+                if (device == AudioSystem.DEVICE_OUT_HDMI ||
+                    device == AudioSystem.DEVICE_OUT_HDMI_ARC ||
+                    device == AudioSystem.DEVICE_OUT_HDMI_EARC) {
+                    mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName,
+                        address, attributes.getAudioProfiles(), attributes.getAudioDescriptors()));
+                } else {
+                    mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address));
+                }
                 mDeviceBroker.postAccessoryPlugMediaUnmute(device);
                 status = true;
             } else if (!connect && isConnected) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0cf55bb..6ba3569 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -9193,16 +9193,6 @@
             mIndexMin = MIN_STREAM_VOLUME[streamType] * 10;
             mIndexMax = MAX_STREAM_VOLUME[streamType] * 10;
 
-            final int status = AudioSystem.initStreamVolume(
-                    streamType, MIN_STREAM_VOLUME[streamType], MAX_STREAM_VOLUME[streamType]);
-            if (status != AudioSystem.AUDIO_STATUS_OK) {
-                sLifecycleLogger.enqueue(new EventLogger.StringEvent(
-                         "VSS() stream:" + streamType + " initStreamVolume=" + status)
-                        .printLog(ALOGE, TAG));
-                sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0,
-                        "VSS()" /*obj*/, 2 * INDICATE_SYSTEM_READY_RETRY_DELAY_MS);
-            }
-
             updateIndexFactors();
             mIndexMinNoPerm = mIndexMin; // may be overwritten later in updateNoPermMinIndex()
 
@@ -9267,6 +9257,19 @@
                     mIndexMinNoPerm = mIndexMin;
                 }
             }
+
+            final int status = AudioSystem.initStreamVolume(
+                    mStreamType, mIndexMin / 10, mIndexMax / 10);
+            sVolumeLogger.enqueue(new EventLogger.StringEvent(
+                    "updateIndexFactors() stream:" + mStreamType + " index min/max:"
+                            + mIndexMin / 10 + "/" + mIndexMax / 10 + " indexStepFactor:"
+                            + mIndexStepFactor).printSlog(ALOGI, TAG));
+            if (status != AudioSystem.AUDIO_STATUS_OK) {
+                sVolumeLogger.enqueue(new EventLogger.StringEvent(
+                        "Failed initStreamVolume with status=" + status).printSlog(ALOGE, TAG));
+                sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0,
+                        "updateIndexFactors()" /*obj*/, 2 * INDICATE_SYSTEM_READY_RETRY_DELAY_MS);
+            }
         }
 
         /**
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 00280c8f..6578023 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -288,11 +288,13 @@
          * @param handler The handler to run {@link #onChange} on, or null if none.
          */
         public SettingObserver(Context context, Handler handler,
-                List<BiometricService.EnabledOnKeyguardCallback> callbacks) {
+                List<BiometricService.EnabledOnKeyguardCallback> callbacks,
+                UserManager userManager, FingerprintManager fingerprintManager,
+                FaceManager faceManager) {
             super(handler);
             mContentResolver = context.getContentResolver();
             mCallbacks = callbacks;
-            mUserManager = context.getSystemService(UserManager.class);
+            mUserManager = userManager;
 
             final boolean hasFingerprint = context.getPackageManager()
                     .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
@@ -304,7 +306,7 @@
                     Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.Q
                     && hasFace && !hasFingerprint;
 
-            addBiometricListenersForMandatoryBiometrics(context);
+            addBiometricListenersForMandatoryBiometrics(context, fingerprintManager, faceManager);
             updateContentObserver();
         }
 
@@ -431,11 +433,21 @@
 
         public boolean getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(int userId) {
             if (!mMandatoryBiometricsEnabled.containsKey(userId)) {
+                Slog.d(TAG, "update mb toggle for user " + userId);
                 updateMandatoryBiometricsForAllProfiles(userId);
             }
             if (!mMandatoryBiometricsRequirementsSatisfied.containsKey(userId)) {
+                Slog.d(TAG, "update mb reqs for user " + userId);
                 updateMandatoryBiometricsRequirementsForAllProfiles(userId);
             }
+
+            Slog.d(TAG, mMandatoryBiometricsEnabled.getOrDefault(userId,
+                    DEFAULT_MANDATORY_BIOMETRICS_STATUS)
+                    + " " + mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId,
+                    DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS)
+                    + " " + getEnabledForApps(userId)
+                    + " " + (mFingerprintEnrolledForUser.getOrDefault(userId, false /* default */)
+                    || mFaceEnrolledForUser.getOrDefault(userId, false /* default */)));
             return mMandatoryBiometricsEnabled.getOrDefault(userId,
                     DEFAULT_MANDATORY_BIOMETRICS_STATUS)
                     && mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId,
@@ -456,11 +468,23 @@
 
         private void updateMandatoryBiometricsForAllProfiles(int userId) {
             int effectiveUserId = userId;
-            if (mUserManager.getMainUser() != null) {
-                effectiveUserId = mUserManager.getMainUser().getIdentifier();
+            final UserInfo parentProfile = mUserManager.getProfileParent(userId);
+
+            if (parentProfile != null) {
+                effectiveUserId = parentProfile.id;
             }
-            for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) {
-                mMandatoryBiometricsEnabled.put(profileUserId,
+
+            final int[] enabledProfileIds = mUserManager.getEnabledProfileIds(effectiveUserId);
+            if (enabledProfileIds != null) {
+                for (int profileUserId : enabledProfileIds) {
+                    mMandatoryBiometricsEnabled.put(profileUserId,
+                            Settings.Secure.getIntForUser(
+                                    mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS,
+                                    DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0,
+                                    effectiveUserId) != 0);
+                }
+            } else {
+                mMandatoryBiometricsEnabled.put(userId,
                         Settings.Secure.getIntForUser(
                                 mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS,
                                 DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0,
@@ -470,11 +494,24 @@
 
         private void updateMandatoryBiometricsRequirementsForAllProfiles(int userId) {
             int effectiveUserId = userId;
-            if (mUserManager.getMainUser() != null) {
-                effectiveUserId = mUserManager.getMainUser().getIdentifier();
+            final UserInfo parentProfile = mUserManager.getProfileParent(userId);
+
+            if (parentProfile != null) {
+                effectiveUserId = parentProfile.id;
             }
-            for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) {
-                mMandatoryBiometricsRequirementsSatisfied.put(profileUserId,
+
+            final int[] enabledProfileIds = mUserManager.getEnabledProfileIds(effectiveUserId);
+            if (enabledProfileIds != null) {
+                for (int profileUserId : enabledProfileIds) {
+                    mMandatoryBiometricsRequirementsSatisfied.put(profileUserId,
+                            Settings.Secure.getIntForUser(mContentResolver,
+                                    Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
+                                    DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS
+                                            ? 1 : 0,
+                                    effectiveUserId) != 0);
+                }
+            } else {
+                mMandatoryBiometricsRequirementsSatisfied.put(userId,
                         Settings.Secure.getIntForUser(mContentResolver,
                                 Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
                                 DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS ? 1 : 0,
@@ -482,10 +519,8 @@
             }
         }
 
-        private void addBiometricListenersForMandatoryBiometrics(Context context) {
-            final FingerprintManager fingerprintManager = context.getSystemService(
-                    FingerprintManager.class);
-            final FaceManager faceManager = context.getSystemService(FaceManager.class);
+        private void addBiometricListenersForMandatoryBiometrics(Context context,
+                FingerprintManager fingerprintManager, FaceManager faceManager) {
             if (fingerprintManager != null) {
                 fingerprintManager.addAuthenticatorsRegisteredCallback(
                         new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -1169,7 +1204,9 @@
          */
         public SettingObserver getSettingObserver(Context context, Handler handler,
                 List<EnabledOnKeyguardCallback> callbacks) {
-            return new SettingObserver(context, handler, callbacks);
+            return new SettingObserver(context, handler, callbacks, context.getSystemService(
+                    UserManager.class), context.getSystemService(FingerprintManager.class),
+                    context.getSystemService(FaceManager.class));
         }
 
         /**
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 4ad7c10..d2c044f 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -255,6 +255,11 @@
     public static final int DIFF_MODE_ID = 1 << 7;
 
     /**
+     * Diff result: The frame rate override list differs.
+     */
+    public static final int DIFF_FRAME_RATE_OVERRIDE = 1 << 8;
+
+    /**
      * Diff result: Catch-all for "everything changed"
      */
     public static final int DIFF_EVERYTHING = 0XFFFFFFFF;
@@ -523,6 +528,9 @@
         if (modeId != other.modeId) {
             diff |= DIFF_MODE_ID;
         }
+        if (!Arrays.equals(frameRateOverrides, other.frameRateOverrides)) {
+            diff |= DIFF_FRAME_RATE_OVERRIDE;
+        }
         if (!Objects.equals(name, other.name)
                 || !Objects.equals(uniqueId, other.uniqueId)
                 || width != other.width
@@ -546,7 +554,6 @@
                 || !Objects.equals(deviceProductInfo, other.deviceProductInfo)
                 || ownerUid != other.ownerUid
                 || !Objects.equals(ownerPackageName, other.ownerPackageName)
-                || !Arrays.equals(frameRateOverrides, other.frameRateOverrides)
                 || !BrightnessSynchronizer.floatEquals(brightnessMinimum, other.brightnessMinimum)
                 || !BrightnessSynchronizer.floatEquals(brightnessMaximum, other.brightnessMaximum)
                 || !BrightnessSynchronizer.floatEquals(brightnessDefault,
diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
index 086f8a9..5f7bc4e 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
@@ -27,6 +27,7 @@
 import com.android.server.display.DisplayManagerService.SyncRoot;
 import com.android.server.display.utils.DebugUtils;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
@@ -177,18 +178,22 @@
                         "handleDisplayDeviceChanged");
             }
             int diff = device.mDebugLastLoggedDeviceInfo.diff(info);
-            if (diff == DisplayDeviceInfo.DIFF_STATE) {
+            if (diff == 0) {
+                Slog.i(TAG, "Display device same: " + info);
+            } else if (diff == DisplayDeviceInfo.DIFF_STATE) {
                 Slog.i(TAG, "Display device changed state: \"" + info.name
                         + "\", " + Display.stateToString(info.state));
             } else if (diff == DisplayDeviceInfo.DIFF_ROTATION) {
                 Slog.i(TAG, "Display device rotated: \"" + info.name
                         + "\", " + Surface.rotationToString(info.rotation));
-            } else if (diff
-                    == (DisplayDeviceInfo.DIFF_MODE_ID | DisplayDeviceInfo.DIFF_RENDER_TIMINGS)) {
+            } else if ((diff &
+                    (DisplayDeviceInfo.DIFF_MODE_ID | DisplayDeviceInfo.DIFF_RENDER_TIMINGS
+                            | DisplayDeviceInfo.DIFF_FRAME_RATE_OVERRIDE)) != 0) {
                 Slog.i(TAG, "Display device changed render timings: \"" + info.name
                         + "\", renderFrameRate=" + info.renderFrameRate
                         + ", presentationDeadlineNanos=" + info.presentationDeadlineNanos
-                        + ", appVsyncOffsetNanos=" + info.appVsyncOffsetNanos);
+                        + ", appVsyncOffsetNanos=" + info.appVsyncOffsetNanos
+                        + ", frameRateOverrides=" + Arrays.toString(info.frameRateOverrides));
             } else if (diff == DisplayDeviceInfo.DIFF_COMMITTED_STATE) {
                 if (DEBUG) {
                     Slog.i(TAG, "Display device changed committed state: \"" + info.name
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 0e77040..5a2610b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -96,6 +96,7 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayGroupListener;
 import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener;
+import android.hardware.display.DisplayTopology;
 import android.hardware.display.DisplayViewport;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -118,6 +119,7 @@
 import android.os.IThermalService;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PermissionEnforcer;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
@@ -4321,6 +4323,10 @@
 
     @VisibleForTesting
     final class BinderService extends IDisplayManager.Stub {
+        BinderService() {
+            super(PermissionEnforcer.fromContext(getContext()));
+        }
+
         /**
          * Returns information about the specified logical display.
          *
@@ -5202,6 +5208,25 @@
             }
             return ddc.getDefaultDozeBrightness();
         }
+
+        @EnforcePermission(MANAGE_DISPLAYS)
+        @Override // Binder call
+        public DisplayTopology getDisplayTopology() {
+            getDisplayTopology_enforcePermission();
+            if (mDisplayTopologyCoordinator == null) {
+                return null;
+            }
+            return mDisplayTopologyCoordinator.getTopology();
+        }
+
+        @EnforcePermission(MANAGE_DISPLAYS)
+        @Override // Binder call
+        public void setDisplayTopology(DisplayTopology topology) {
+            setDisplayTopology_enforcePermission();
+            if (mDisplayTopologyCoordinator != null) {
+                mDisplayTopologyCoordinator.setTopology(topology);
+            }
+        }
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
index b101e58..4722686 100644
--- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
+++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import android.hardware.display.DisplayTopology;
 import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -33,7 +34,7 @@
 class DisplayTopologyCoordinator {
 
     @GuardedBy("mLock")
-    private final DisplayTopology mTopology;
+    private DisplayTopology mTopology;
 
     /**
      * Check if extended displays are enabled. If not, a topology is not needed.
@@ -76,6 +77,21 @@
     }
 
     /**
+     * @return A deep copy of the topology.
+     */
+    DisplayTopology getTopology() {
+        synchronized (mLock) {
+            return mTopology;
+        }
+    }
+
+    void setTopology(DisplayTopology topology) {
+        synchronized (mLock) {
+            mTopology = topology;
+        }
+    }
+
+    /**
      * Print the object's state and debug information into the given stream.
      * @param pw The stream to dump information to.
      */
@@ -108,6 +124,7 @@
                 && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP;
     }
 
+    @VisibleForTesting
     static class Injector {
         DisplayTopology getTopology() {
             return new DisplayTopology();
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 794eb87..0c04be1 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.service.dreams.Flags.cleanupDreamSettingsOnUninstall;
 import static android.service.dreams.Flags.dreamHandlesBeingObscured;
 
 import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID;
@@ -353,28 +354,32 @@
     @Override
     public void onUserStarting(@NonNull TargetUser user) {
         super.onUserStarting(user);
-        mHandler.post(() -> {
-            final int userId = user.getUserIdentifier();
-            if (!mPackageMonitors.contains(userId)) {
-                final PackageMonitor monitor = new PerUserPackageMonitor();
-                monitor.register(mContext, UserHandle.of(userId), mHandler);
-                mPackageMonitors.put(userId, monitor);
-            } else {
-                Slog.w(TAG, "Package monitor already registered for " + userId);
-            }
-        });
+        if (cleanupDreamSettingsOnUninstall()) {
+            mHandler.post(() -> {
+                final int userId = user.getUserIdentifier();
+                if (!mPackageMonitors.contains(userId)) {
+                    final PackageMonitor monitor = new PerUserPackageMonitor();
+                    monitor.register(mContext, UserHandle.of(userId), mHandler);
+                    mPackageMonitors.put(userId, monitor);
+                } else {
+                    Slog.w(TAG, "Package monitor already registered for " + userId);
+                }
+            });
+        }
     }
 
     @Override
     public void onUserStopping(@NonNull TargetUser user) {
         super.onUserStopping(user);
-        mHandler.post(() -> {
-            final PackageMonitor monitor = mPackageMonitors.removeReturnOld(
-                    user.getUserIdentifier());
-            if (monitor != null) {
-                monitor.unregister();
-            }
-        });
+        if (cleanupDreamSettingsOnUninstall()) {
+            mHandler.post(() -> {
+                final PackageMonitor monitor = mPackageMonitors.removeReturnOld(
+                        user.getUserIdentifier());
+                if (monitor != null) {
+                    monitor.unregister();
+                }
+            });
+        }
     }
 
     private void dumpInternal(PrintWriter pw) {
@@ -715,15 +720,23 @@
                         userId));
         if (componentNames != null) {
             // Filter out any components in the removed package.
-            final ComponentName[] filteredComponents = Arrays.stream(componentNames).filter(
-                    (componentName -> !TextUtils.equals(componentName.getPackageName(),
-                            packageName))).toArray(ComponentName[]::new);
+            final ComponentName[] filteredComponents =
+                    Arrays.stream(componentNames)
+                            .filter((componentName -> !isSamePackage(packageName, componentName)))
+                            .toArray(ComponentName[]::new);
             if (filteredComponents.length != componentNames.length) {
                 setDreamComponentsForUser(userId, filteredComponents);
             }
         }
     }
 
+    private static boolean isSamePackage(String packageName, ComponentName componentName) {
+        if (packageName == null || componentName == null) {
+            return false;
+        }
+        return TextUtils.equals(componentName.getPackageName(), packageName);
+    }
+
     private void setDreamComponentsForUser(int userId, ComponentName[] componentNames) {
         Settings.Secure.putStringForUser(mContext.getContentResolver(),
                 Settings.Secure.SCREENSAVER_COMPONENTS,
@@ -884,7 +897,10 @@
         }
         StringBuilder names = new StringBuilder();
         for (ComponentName componentName : componentNames) {
-            if (names.length() > 0) {
+            if (componentName == null) {
+                continue;
+            }
+            if (!names.isEmpty()) {
                 names.append(',');
             }
             names.append(componentName.flattenToString());
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index f2e2f65..5b4c033 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -1001,7 +1001,7 @@
             try {
                 // Create an AIDL callback that can callback onHotplugEvent
                 mHdmiConnection.setCallback(new HdmiConnectionCallbackAidl(callback));
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 HdmiLogger.error("Couldn't initialise tv.hdmi callback : ", e);
             }
         }
@@ -1134,7 +1134,7 @@
                     i++;
                 }
                 return hdmiPortInfo;
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 HdmiLogger.error("Failed to get port information : ", e);
                 return null;
             }
@@ -1144,7 +1144,7 @@
         public boolean nativeIsConnected(int port) {
             try {
                 return mHdmiConnection.isConnected(port);
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 HdmiLogger.error("Failed to get connection info : ", e);
                 return false;
             }
@@ -1158,7 +1158,7 @@
                 HdmiLogger.error(
                         "Could not set HPD signal type for portId " + portId + " to " + signal
                                 + ". Error: ", sse.errorCode);
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 HdmiLogger.error(
                         "Could not set HPD signal type for portId " + portId + " to " + signal
                                 + ". Exception: ", e);
@@ -1169,7 +1169,7 @@
         public int nativeGetHpdSignalType(int portId) {
             try {
                 return mHdmiConnection.getHpdSignal(portId);
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 HdmiLogger.error(
                         "Could not get HPD signal type for portId " + portId + ". Exception: ", e);
                 return Constants.HDMI_HPD_TYPE_PHYSICAL;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index bf415a3..7505c71 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -646,9 +646,9 @@
         int address = message.getSource();
         int type = message.getParams()[2];
 
-        if (!ActiveSource.of(address, path).equals(getActiveSource())) {
-            HdmiLogger.debug("Check if a new device is connected to the active path");
-            handleNewDeviceAtTheTailOfActivePath(path);
+        if (getActiveSource().logicalAddress != address && getActivePath() == path) {
+            HdmiLogger.debug("New logical address detected on the current active path.");
+            startRoutingControl(path, path, null);
         }
         startNewDeviceAction(ActiveSource.of(address, path), type);
         return Constants.HANDLED;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 132d6fa..0c5069f 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -771,6 +771,14 @@
             Slog.i(TAG, "Device does not support eARC.");
         }
         mHdmiCecNetwork = new HdmiCecNetwork(this, mCecController, mMhlController);
+        if (isTvDevice() && getWasCecDisabledOnStandbyByLowEnergyMode()) {
+            Slog.w(TAG, "Re-enable CEC on boot-up since it was disabled due to low energy "
+                    + " mode.");
+            getHdmiCecConfig().setIntValue(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+                    HDMI_CEC_CONTROL_ENABLED);
+            setWasCecDisabledOnStandbyByLowEnergyMode(false);
+            setCecEnabled(HDMI_CEC_CONTROL_ENABLED);
+        }
         if (isCecControlEnabled()) {
             initializeCec(INITIATED_BY_BOOT_UP);
         } else {
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index e545dd5..6f35402 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -215,6 +215,18 @@
             systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_T,
                     KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
                     KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_MINUS,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_EQUALS,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_S,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK));
         }
         if (keyboardA11yShortcutControl()) {
             if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 155ffe8..0124e25 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -20,6 +20,7 @@
 import static android.content.pm.PackageManager.FEATURE_WATCH;
 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;
 
@@ -750,6 +751,28 @@
                     }
                 }
                 break;
+            case KeyEvent.KEYCODE_LOCK:
+                if (enableNew25q2Keycodes()) {
+                    if (firstDown) {
+                        handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_LOCK},
+                                /* modifierState = */0,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
+                                /* flags = */0, /* appLaunchData = */null);
+                    }
+                }
+                return true;
+            case KeyEvent.KEYCODE_FULLSCREEN:
+                if (enableNew25q2Keycodes()) {
+                    if (firstDown) {
+                        handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_FULLSCREEN},
+                                /* modifierState = */0,
+                                KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+                                KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
+                                /* flags = */0, /* appLaunchData = */null);
+                    }
+                }
+                return true;
             case KeyEvent.KEYCODE_ASSIST:
                 Slog.wtf(TAG, "KEYCODE_ASSIST should be handled in interceptKeyBeforeQueueing");
                 return true;
@@ -764,6 +787,9 @@
                 Slog.wtf(TAG, "KEYCODE_STYLUS_BUTTON_* should be handled in"
                         + " interceptKeyBeforeQueueing");
                 return true;
+            case KeyEvent.KEYCODE_DO_NOT_DISTURB:
+                // TODO(b/365920375): Implement 25Q2 keycode implementation in system
+                return true;
         }
 
         // Handle custom shortcuts
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index acc8f66..f611c57 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -35,6 +35,7 @@
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
 import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.HubInfo;
 import android.hardware.location.IContextHubCallback;
 import android.hardware.location.IContextHubClient;
 import android.hardware.location.IContextHubClientCallback;
@@ -57,6 +58,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Pair;
 import android.util.proto.ProtoOutputStream;
@@ -134,6 +136,9 @@
     private Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
     private List<String> mSupportedContextHubPerms;
     private List<ContextHubInfo> mContextHubInfoList;
+
+    @Nullable private final HubInfoRegistry mHubInfoRegistry;
+
     private final RemoteCallbackList<IContextHubCallback> mCallbacksList =
             new RemoteCallbackList<>();
 
@@ -309,10 +314,21 @@
         mContext = context;
         long startTimeNs = SystemClock.elapsedRealtimeNanos();
         mContextHubWrapper = contextHubWrapper;
+
         if (!initContextHubServiceState(startTimeNs)) {
             Log.e(TAG, "Failed to initialize the Context Hub Service");
+            mHubInfoRegistry = null;
             return;
         }
+
+        if (Flags.offloadApi()) {
+            mHubInfoRegistry = new HubInfoRegistry(mContextHubWrapper);
+            Log.i(TAG, "Enabling generic offload API");
+        } else {
+            mHubInfoRegistry = null;
+            Log.i(TAG, "Disabling generic offload API");
+        }
+
         initDefaultClientMap();
 
         initLocationSettingNotifications();
@@ -427,7 +443,7 @@
 
         Pair<List<ContextHubInfo>, List<String>> hubInfo;
         try {
-            hubInfo = mContextHubWrapper.getHubs();
+            hubInfo = mContextHubWrapper.getContextHubs();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException while getting Context Hub info", e);
             hubInfo = new Pair<>(Collections.emptyList(), Collections.emptyList());
@@ -713,6 +729,16 @@
         return mContextHubInfoList;
     }
 
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public List<HubInfo> getHubs() throws RemoteException {
+        super.getHubs_enforcePermission();
+        if (mHubInfoRegistry == null) {
+            return Collections.emptyList();
+        }
+        return mHubInfoRegistry.getHubs();
+    }
+
     /**
      * Creates an internal load transaction callback to be used for old API clients
      *
@@ -1417,6 +1443,8 @@
             }
         }
 
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
+        pw = ipw;
         pw.println("Dumping ContextHub Service");
 
         pw.println("");
@@ -1428,6 +1456,11 @@
         pw.println("Supported permissions: "
                 + Arrays.toString(mSupportedContextHubPerms.toArray()));
         pw.println("");
+
+        if (mHubInfoRegistry != null) {
+            mHubInfoRegistry.dump(ipw);
+        }
+
         pw.println("=================== NANOAPPS ====================");
         // Dump nanoAppHash
         mNanoAppStateManager.foreachNanoAppInstanceInfo(pw::println);
diff --git a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
new file mode 100644
index 0000000..68de9db
--- /dev/null
+++ b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java
@@ -0,0 +1,60 @@
+/*
+ * 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.location.contexthub;
+
+import android.hardware.location.HubInfo;
+import android.os.RemoteException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.List;
+
+class HubInfoRegistry {
+    private static final String TAG = "HubInfoRegistry";
+
+    private final IContextHubWrapper mContextHubWrapper;
+
+    private final List<HubInfo> mHubsInfo;
+
+    HubInfoRegistry(IContextHubWrapper contextHubWrapper) {
+        List<HubInfo> hubInfos;
+        mContextHubWrapper = contextHubWrapper;
+        try {
+            hubInfos = mContextHubWrapper.getHubs();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException while getting Hub info", e);
+            hubInfos = Collections.emptyList();
+        }
+        mHubsInfo = hubInfos;
+    }
+
+    /** Retrieve the list of hubs available. */
+    List<HubInfo> getHubs() {
+        return mHubsInfo;
+    }
+
+    void dump(IndentingPrintWriter ipw) {
+        ipw.println(TAG);
+
+        ipw.increaseIndent();
+        for (HubInfo hubInfo : mHubsInfo) {
+            ipw.println(hubInfo);
+        }
+        ipw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index 5e9277a..6656a6f 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -30,9 +30,11 @@
 import android.hardware.contexthub.V1_2.IContexthubCallback;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.HubInfo;
 import android.hardware.location.NanoAppBinary;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
+import android.hardware.location.VendorHubInfo;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -52,13 +54,14 @@
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * @hide
  */
 public abstract class IContextHubWrapper {
+    private static final boolean DEBUG = false;
     private static final String TAG = "IContextHubWrapper";
 
     /**
@@ -217,10 +220,14 @@
         return proxy == null ? null : new ContextHubWrapperAidl(proxy);
     }
 
-    /**
-     * Calls the appropriate getHubs function depending on the HAL version.
-     */
-    public abstract Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException;
+    /** Calls the appropriate getHubs function depending on the HAL version. */
+    public abstract Pair<List<ContextHubInfo>, List<String>> getContextHubs()
+            throws RemoteException;
+
+    /** Calls the appropriate getHubs function depending on the HAL version. */
+    public List<HubInfo> getHubs() throws RemoteException {
+        return Collections.emptyList();
+    }
 
     /**
      * @return True if this version of the Contexthub HAL supports Location setting notifications.
@@ -556,7 +563,7 @@
             mIsTestModeEnabled.set(false);
         }
 
-        public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
+        public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException {
             android.hardware.contexthub.IContextHub hub = getHub();
             if (hub == null) {
                 return new Pair<List<ContextHubInfo>, List<String>>(new ArrayList<ContextHubInfo>(),
@@ -574,6 +581,47 @@
             return new Pair(hubInfoList, new ArrayList<String>(supportedPermissions));
         }
 
+        public List<HubInfo> getHubs() throws RemoteException {
+            android.hardware.contexthub.IContextHub hub = getHub();
+            if (hub == null) {
+                return Collections.emptyList();
+            }
+
+            List<HubInfo> retVal = new ArrayList<>();
+            final List<android.hardware.contexthub.HubInfo> halHubs = hub.getHubs();
+
+            for (android.hardware.contexthub.HubInfo halHub : halHubs) {
+                /* HAL -> API Type conversion */
+                final HubInfo hubInfo;
+                switch (halHub.hubDetails.getTag()) {
+                    case android.hardware.contexthub.HubInfo.HubDetails.contextHubInfo:
+                        ContextHubInfo contextHubInfo =
+                                new ContextHubInfo(halHub.hubDetails.getContextHubInfo());
+                        hubInfo = new HubInfo(halHub.hubId, contextHubInfo);
+                        break;
+                    case android.hardware.contexthub.HubInfo.HubDetails.vendorHubInfo:
+                        VendorHubInfo vendorHubInfo =
+                                new VendorHubInfo(halHub.hubDetails.getVendorHubInfo());
+                        hubInfo = new HubInfo(halHub.hubId, vendorHubInfo);
+                        break;
+                    default:
+                        Log.w(TAG, "getHubs: invalid hub: " + halHub);
+                        // Invalid
+                        continue;
+                }
+
+                if (DEBUG) {
+                    Log.i(TAG, "getHubs: hubInfo=" + hubInfo);
+                }
+                retVal.add(hubInfo);
+            }
+
+            if (DEBUG) {
+                Log.i(TAG, "getHubs: total count=" + retVal.size());
+            }
+            return retVal;
+        }
+
         public boolean supportsLocationSettingNotifications() {
             return true;
         }
@@ -1061,7 +1109,7 @@
             mHub = hub;
         }
 
-        public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
+        public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException {
             ArrayList<ContextHubInfo> hubInfoList = new ArrayList<>();
             for (ContextHub hub : mHub.getHubs()) {
                 hubInfoList.add(new ContextHubInfo(hub));
@@ -1106,7 +1154,7 @@
             mHub = hub;
         }
 
-        public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
+        public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException {
             ArrayList<ContextHubInfo> hubInfoList = new ArrayList<>();
             for (ContextHub hub : mHub.getHubs()) {
                 hubInfoList.add(new ContextHubInfo(hub));
@@ -1170,7 +1218,7 @@
             mHubInfo = new Pair(hubInfoList, supportedPermissions);
         }
 
-        public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException {
+        public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException {
             mHub.getHubs_1_2(this);
             return mHubInfo;
         }
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index a45ea1d..21ae182 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -59,15 +59,15 @@
             return pp;
         }
         @Override
-        public void updatePictureProfile(long id, PictureProfile pp) {
+        public void updatePictureProfile(String id, PictureProfile pp) {
             // TODO: implement
         }
         @Override
-        public void removePictureProfile(long id) {
+        public void removePictureProfile(String id) {
             // TODO: implement
         }
         @Override
-        public PictureProfile getPictureProfileById(long id) {
+        public PictureProfile getPictureProfile(int type, String name) {
             return null;
         }
         @Override
@@ -79,7 +79,7 @@
             return new ArrayList<>();
         }
         @Override
-        public List<PictureProfile> getAllPictureProfiles() {
+        public List<String> getPictureProfilePackageNames() {
             return new ArrayList<>();
         }
 
@@ -89,15 +89,15 @@
             return pp;
         }
         @Override
-        public void updateSoundProfile(long id, SoundProfile pp) {
+        public void updateSoundProfile(String id, SoundProfile pp) {
             // TODO: implement
         }
         @Override
-        public void removeSoundProfile(long id) {
+        public void removeSoundProfile(String id) {
             // TODO: implement
         }
         @Override
-        public SoundProfile getSoundProfileById(long id) {
+        public SoundProfile getSoundProfileById(String id) {
             return null;
         }
         @Override
@@ -109,7 +109,7 @@
             return new ArrayList<>();
         }
         @Override
-        public List<SoundProfile> getAllSoundProfiles() {
+        public List<String> getSoundProfilePackageNames() {
             return new ArrayList<>();
         }
 
@@ -138,6 +138,14 @@
             return new ArrayList<>();
         }
 
+        @Override
+        public List<String> getPictureProfileAllowList() {
+            return new ArrayList<>();
+        }
+
+        @Override
+        public void setPictureProfileAllowList(List<String> packages) {
+        }
 
         @Override
         public boolean isSupported() {
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 2a3be1e..7de2815 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -513,12 +513,6 @@
     private boolean mLoadedRestrictBackground;
 
     /**
-     * Whether or not network for apps in proc-states greater than
-     * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} is always blocked.
-     */
-    private boolean mBackgroundNetworkRestricted;
-
-    /**
      * Whether or not metered firewall chains should be used for uid policy controlling access to
      * metered networks.
      */
@@ -1117,14 +1111,7 @@
                         writePolicyAL();
                     }
 
-                    // The flag is boot-stable.
-                    mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove();
-                    if (mBackgroundNetworkRestricted) {
-                        // Firewall rules and UidBlockedState will get updated in
-                        // updateRulesForGlobalChangeAL below.
-                        enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true);
-                    }
-
+                    enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true);
                     setRestrictBackgroundUL(mLoadedRestrictBackground, "init_service");
                     updateRulesForGlobalChangeAL(false);
                     updateNotificationsNL();
@@ -1135,11 +1122,8 @@
                 final int changes = ActivityManager.UID_OBSERVER_PROCSTATE
                         | ActivityManager.UID_OBSERVER_GONE
                         | ActivityManager.UID_OBSERVER_CAPABILITY;
-
-                final int cutpoint = mBackgroundNetworkRestricted ? PROCESS_STATE_UNKNOWN
-                        : NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE;
                 mActivityManagerInternal.registerNetworkPolicyUidObserver(mUidObserver, changes,
-                        cutpoint, "android");
+                        PROCESS_STATE_UNKNOWN, "android");
                 mNetworkManager.registerObserver(mAlertObserver);
             } catch (RemoteException e) {
                 // ignored; both services live in system_server
@@ -1280,21 +1264,19 @@
                 // different chains may change.
                 return true;
             }
-            if (mBackgroundNetworkRestricted) {
-                if ((previousProcState >= BACKGROUND_THRESHOLD_STATE)
+            if ((previousProcState >= BACKGROUND_THRESHOLD_STATE)
                     != (newProcState >= BACKGROUND_THRESHOLD_STATE)) {
-                    // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: The network rules will
-                    // need to be re-evaluated for the background chain.
-                    return true;
-                }
-                if (mUseDifferentDelaysForBackgroundChain
-                        && newProcState >= BACKGROUND_THRESHOLD_STATE
-                        && getBackgroundTransitioningDelay(newProcState)
-                        < getBackgroundTransitioningDelay(previousProcState)) {
-                    // The old and new proc-state both are in the blocked state but the background
-                    // transition delay is reduced, so we may have to update the rules sooner.
-                    return true;
-                }
+                // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: The network rules will
+                // need to be re-evaluated for the background chain.
+                return true;
+            }
+            if (mUseDifferentDelaysForBackgroundChain
+                    && newProcState >= BACKGROUND_THRESHOLD_STATE
+                    && getBackgroundTransitioningDelay(newProcState)
+                    < getBackgroundTransitioningDelay(previousProcState)) {
+                // The old and new proc-state both are in the blocked state but the background
+                // transition delay is reduced, so we may have to update the rules sooner.
+                return true;
             }
             final int networkCapabilities = PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK
                     | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK;
@@ -1367,9 +1349,7 @@
             // on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected
             synchronized (mUidRulesFirstLock) {
                 updatePowerSaveAllowlistUL();
-                if (mBackgroundNetworkRestricted) {
-                    updateRulesForBackgroundChainUL();
-                }
+                updateRulesForBackgroundChainUL();
                 updateRulesForRestrictPowerUL();
                 updateRulesForAppIdleUL();
             }
@@ -4100,8 +4080,6 @@
 
                 fout.println();
                 fout.println("Flags:");
-                fout.println(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE + ": "
-                        + mBackgroundNetworkRestricted);
                 fout.println(Flags.FLAG_USE_METERED_FIREWALL_CHAINS + ": "
                         + mUseMeteredFirewallChains);
                 fout.println(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN + ": "
@@ -4251,36 +4229,34 @@
                     fout.decreaseIndent();
                 }
 
-                if (mBackgroundNetworkRestricted) {
+                fout.println();
+                if (mUseDifferentDelaysForBackgroundChain) {
+                    fout.print("Background restrictions short delay: ");
+                    TimeUtils.formatDuration(mBackgroundRestrictionShortDelayMs, fout);
                     fout.println();
-                    if (mUseDifferentDelaysForBackgroundChain) {
-                        fout.print("Background restrictions short delay: ");
-                        TimeUtils.formatDuration(mBackgroundRestrictionShortDelayMs, fout);
-                        fout.println();
 
-                        fout.print("Background restrictions long delay: ");
-                        TimeUtils.formatDuration(mBackgroundRestrictionLongDelayMs, fout);
-                        fout.println();
-                    }
-
-                    size = mBackgroundTransitioningUids.size();
-                    if (size > 0) {
-                        final long nowUptime = SystemClock.uptimeMillis();
-                        fout.println("Uids transitioning to background:");
-                        fout.increaseIndent();
-                        for (int i = 0; i < size; i++) {
-                            fout.print("UID=");
-                            fout.print(mBackgroundTransitioningUids.keyAt(i));
-                            fout.print(", ");
-                            TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i),
-                                    nowUptime, fout);
-                            fout.println();
-                        }
-                        fout.decreaseIndent();
-                    }
+                    fout.print("Background restrictions long delay: ");
+                    TimeUtils.formatDuration(mBackgroundRestrictionLongDelayMs, fout);
                     fout.println();
                 }
 
+                size = mBackgroundTransitioningUids.size();
+                if (size > 0) {
+                    final long nowUptime = SystemClock.uptimeMillis();
+                    fout.println("Uids transitioning to background:");
+                    fout.increaseIndent();
+                    for (int i = 0; i < size; i++) {
+                        fout.print("UID=");
+                        fout.print(mBackgroundTransitioningUids.keyAt(i));
+                        fout.print(", ");
+                        TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i),
+                                nowUptime, fout);
+                        fout.println();
+                    }
+                    fout.decreaseIndent();
+                }
+                fout.println();
+
                 final SparseBooleanArray knownUids = new SparseBooleanArray();
                 collectKeys(mUidState, knownUids);
                 synchronized (mUidBlockedState) {
@@ -4465,51 +4441,49 @@
                     }
                     updatePowerRestrictionRules = true;
                 }
-                if (mBackgroundNetworkRestricted) {
-                    final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground(
-                            oldUidState);
-                    final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState);
-                    if (!wasAllowed && isAllowed) {
-                        mBackgroundTransitioningUids.delete(uid);
-                        updateRuleForBackgroundUL(uid);
-                        updatePowerRestrictionRules = true;
-                    } else if (!isAllowed) {
-                        final int transitionIdx = mBackgroundTransitioningUids.indexOfKey(uid);
-                        final long completionTimeMs = SystemClock.uptimeMillis()
-                                + getBackgroundTransitioningDelay(procState);
-                        boolean completionTimeUpdated = false;
-                        if (wasAllowed) {
-                            // Rules need to transition from allowed to blocked after the respective
-                            // delay.
-                            if (transitionIdx < 0) {
-                                // This is just a defensive check in case the upstream code ever
-                                // makes multiple calls for the same process state change.
-                                mBackgroundTransitioningUids.put(uid, completionTimeMs);
-                                completionTimeUpdated = true;
-                            }
-                        } else if (mUseDifferentDelaysForBackgroundChain) {
-                            // wasAllowed was false, but the transition delay may have reduced.
-                            // Currently, this can happen when the uid transitions from
-                            // LAST_ACTIVITY to CACHED_ACTIVITY, for example.
-                            if (transitionIdx >= 0
-                                    && completionTimeMs < mBackgroundTransitioningUids.valueAt(
-                                    transitionIdx)) {
-                                mBackgroundTransitioningUids.setValueAt(transitionIdx,
-                                        completionTimeMs);
-                                completionTimeUpdated = true;
-                            }
+                final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground(
+                        oldUidState);
+                final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState);
+                if (!wasAllowed && isAllowed) {
+                    mBackgroundTransitioningUids.delete(uid);
+                    updateRuleForBackgroundUL(uid);
+                    updatePowerRestrictionRules = true;
+                } else if (!isAllowed) {
+                    final int transitionIdx = mBackgroundTransitioningUids.indexOfKey(uid);
+                    final long completionTimeMs = SystemClock.uptimeMillis()
+                            + getBackgroundTransitioningDelay(procState);
+                    boolean completionTimeUpdated = false;
+                    if (wasAllowed) {
+                        // Rules need to transition from allowed to blocked after the respective
+                        // delay.
+                        if (transitionIdx < 0) {
+                            // This is just a defensive check in case the upstream code ever
+                            // makes multiple calls for the same process state change.
+                            mBackgroundTransitioningUids.put(uid, completionTimeMs);
+                            completionTimeUpdated = true;
                         }
-                        if (completionTimeUpdated
-                                && completionTimeMs < mNextProcessBackgroundUidsTime) {
-                            // Many uids may be in this "transitioning" state at the same time,
-                            // so we always keep one message to process transition completion at
-                            // the earliest time.
-                            mHandler.removeMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS);
-                            mHandler.sendEmptyMessageAtTime(
-                                    MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs);
-                            mNextProcessBackgroundUidsTime = completionTimeMs;
+                    } else if (mUseDifferentDelaysForBackgroundChain) {
+                        // wasAllowed was false, but the transition delay may have reduced.
+                        // Currently, this can happen when the uid transitions from
+                        // LAST_ACTIVITY to CACHED_ACTIVITY, for example.
+                        if (transitionIdx >= 0
+                                && completionTimeMs < mBackgroundTransitioningUids.valueAt(
+                                transitionIdx)) {
+                            mBackgroundTransitioningUids.setValueAt(transitionIdx,
+                                    completionTimeMs);
+                            completionTimeUpdated = true;
                         }
                     }
+                    if (completionTimeUpdated
+                            && completionTimeMs < mNextProcessBackgroundUidsTime) {
+                        // Many uids may be in this "transitioning" state at the same time,
+                        // so we always keep one message to process transition completion at
+                        // the earliest time.
+                        mHandler.removeMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS);
+                        mHandler.sendEmptyMessageAtTime(
+                                MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs);
+                        mNextProcessBackgroundUidsTime = completionTimeMs;
+                    }
                 }
                 if (mLowPowerStandbyActive) {
                     boolean allowedInLpsChanged =
@@ -4545,12 +4519,10 @@
                 if (mRestrictPower) {
                     updateRuleForRestrictPowerUL(uid);
                 }
-                if (mBackgroundNetworkRestricted) {
-                    // Uid is no longer running, there is no point in any grace period of network
-                    // access during transitions to lower importance proc-states.
-                    mBackgroundTransitioningUids.delete(uid);
-                    updateRuleForBackgroundUL(uid);
-                }
+                // Uid is no longer running, there is no point in any grace period of network
+                // access during transitions to lower importance proc-states.
+                mBackgroundTransitioningUids.delete(uid);
+                updateRuleForBackgroundUL(uid);
                 updateRulesForPowerRestrictionsUL(uid);
                 if (mLowPowerStandbyActive) {
                     updateRuleForLowPowerStandbyUL(uid);
@@ -5021,9 +4993,7 @@
                     "updateRulesForGlobalChangeAL: " + (restrictedNetworksChanged ? "R" : "-"));
         }
         try {
-            if (mBackgroundNetworkRestricted) {
-                updateRulesForBackgroundChainUL();
-            }
+            updateRulesForBackgroundChainUL();
             updateRulesForAppIdleUL();
             updateRulesForRestrictPowerUL();
             updateRulesForRestrictBackgroundUL();
@@ -5183,9 +5153,7 @@
             updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN);
             updateRuleForDeviceIdleUL(uid);
             updateRuleForRestrictPowerUL(uid);
-            if (mBackgroundNetworkRestricted) {
-                updateRuleForBackgroundUL(uid);
-            }
+            updateRuleForBackgroundUL(uid);
             // Update internal rules.
             updateRulesForPowerRestrictionsUL(uid);
         }
@@ -5358,9 +5326,7 @@
         updateRuleForDeviceIdleUL(uid);
         updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN);
         updateRuleForRestrictPowerUL(uid);
-        if (mBackgroundNetworkRestricted) {
-            updateRuleForBackgroundUL(uid);
-        }
+        updateRuleForBackgroundUL(uid);
 
         // If the uid has the necessary permissions, then it should be added to the restricted mode
         // firewall allowlist.
@@ -5611,7 +5577,7 @@
             newBlockedReasons |= (mLowPowerStandbyActive ? BLOCKED_REASON_LOW_POWER_STANDBY : 0);
             newBlockedReasons |= (isUidIdle ? BLOCKED_REASON_APP_STANDBY : 0);
             newBlockedReasons |= (uidBlockedState.blockedReasons & BLOCKED_REASON_RESTRICTED_MODE);
-            newBlockedReasons |= mBackgroundNetworkRestricted ? BLOCKED_REASON_APP_BACKGROUND : 0;
+            newBlockedReasons |= BLOCKED_REASON_APP_BACKGROUND;
 
             newAllowedReasons |= (isSystem(uid) ? ALLOWED_REASON_SYSTEM : 0);
             newAllowedReasons |= (isForeground ? ALLOWED_REASON_FOREGROUND : 0);
@@ -5624,8 +5590,7 @@
                     & ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS);
             newAllowedReasons |= (isAllowlistedFromLowPowerStandbyUL(uid))
                     ? ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST : 0;
-            newAllowedReasons |= (mBackgroundNetworkRestricted
-                    && isUidExemptFromBackgroundRestrictions(uid))
+            newAllowedReasons |= isUidExemptFromBackgroundRestrictions(uid)
                     ? ALLOWED_REASON_NOT_IN_BACKGROUND : 0;
 
             uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig
index 7f04e66..3c0ff61 100644
--- a/services/core/java/com/android/server/net/flags.aconfig
+++ b/services/core/java/com/android/server/net/flags.aconfig
@@ -2,13 +2,6 @@
 container: "system"
 
 flag {
-    name: "network_blocked_for_top_sleeping_and_above"
-    namespace: "backstage_power"
-    description: "Block network access for apps in a low importance background state"
-    bug: "304347838"
-}
-
-flag {
     name: "use_metered_firewall_chains"
     namespace: "backstage_power"
     description: "Use metered firewall chains to control access to metered networks"
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 5febd5c..5914dbe 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -788,6 +788,20 @@
                 return;
             }
 
+            // Check if summary & child notifications are not part of the same section/bundle
+            // Needs a check here if notification was bundled while enqueued
+            if (notificationRegroupOnClassification()
+                    && android.service.notification.Flags.notificationClassification()) {
+                if (isGroupChildBundled(record, summaryByGroupKey)) {
+                    if (DEBUG) {
+                        Slog.v(TAG, "isGroupChildInDifferentBundleThanSummary: " + record);
+                    }
+                    moveNotificationsToNewSection(record.getUserId(), pkgName,
+                            List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)));
+                    return;
+                }
+            }
+
             // scenario 3: sparse/singleton groups
             if (Flags.notificationForceGroupSingletons()) {
                 try {
@@ -800,6 +814,27 @@
         }
     }
 
+    private static boolean isGroupChildBundled(final NotificationRecord record,
+            final Map<String, NotificationRecord> summaryByGroupKey) {
+        final StatusBarNotification sbn = record.getSbn();
+        final String groupKey = record.getSbn().getGroupKey();
+
+        if (!sbn.isAppGroup()) {
+            return false;
+        }
+
+        if (record.getNotification().isGroupSummary()) {
+            return false;
+        }
+
+        final NotificationRecord summary = summaryByGroupKey.get(groupKey);
+        if (summary == null) {
+            return false;
+        }
+
+        return NotificationChannel.SYSTEM_RESERVED_IDS.contains(record.getChannel().getId());
+    }
+
     /**
      * Called when a notification is removed, so that this helper can adjust the aggregate groups:
      *  - Removes the autogroup summary of the notification's section
@@ -1598,7 +1633,7 @@
         final int mSummaryId;
         private final Predicate<NotificationRecord> mSectionChecker;
 
-        public NotificationSectioner(String name, int summaryId,
+        private NotificationSectioner(String name, int summaryId,
                 Predicate<NotificationRecord> sectionChecker) {
             mName = name;
             mSummaryId = summaryId;
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 93482e7..b0ef807 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -75,9 +75,7 @@
 import com.android.internal.util.function.TriPredicate;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
-import com.android.server.LocalServices;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
-import com.android.server.pm.UserManagerInternal;
 import com.android.server.utils.TimingsTraceAndSlog;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -136,7 +134,6 @@
     private final UserProfiles mUserProfiles;
     protected final IPackageManager mPm;
     protected final UserManager mUm;
-    private final UserManagerInternal mUserManagerInternal;
     private final Config mConfig;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
@@ -198,7 +195,6 @@
         mConfig = getConfig();
         mApprovalLevel = APPROVAL_BY_COMPONENT;
         mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-        mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
     }
 
     abstract protected Config getConfig();
@@ -1389,14 +1385,9 @@
     @GuardedBy("mMutex")
     protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind,
             final IntArray activeUsers,
-            SparseArray<ArraySet<ComponentName>> approvedComponentsByUser,
-            boolean isVisibleBackgroundUser) {
-        // When it is a visible background user in Automotive MUMD environment,
-        // don't clear mEnabledServicesForCurrentProfile and mEnabledServicesPackageNames.
-        if (!isVisibleBackgroundUser) {
-            mEnabledServicesForCurrentProfiles.clear();
-            mEnabledServicesPackageNames.clear();
-        }
+            SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) {
+        mEnabledServicesForCurrentProfiles.clear();
+        mEnabledServicesPackageNames.clear();
         final int nUserIds = activeUsers.size();
 
         for (int i = 0; i < nUserIds; ++i) {
@@ -1417,12 +1408,7 @@
             }
 
             componentsToBind.put(userId, add);
-            // When it is a visible background user in Automotive MUMD environment,
-            // skip adding items to mEnabledServicesForCurrentProfile
-            // and mEnabledServicesPackageNames.
-            if (isVisibleBackgroundUser) {
-                continue;
-            }
+
             mEnabledServicesForCurrentProfiles.addAll(userComponents);
 
             for (int j = 0; j < userComponents.size(); j++) {
@@ -1470,10 +1456,7 @@
         IntArray userIds = mUserProfiles.getCurrentProfileIds();
         boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext)
                 && allowRebindForParentUser();
-        boolean isVisibleBackgroundUser = false;
         if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
-            isVisibleBackgroundUser =
-                    mUserManagerInternal.isVisibleBackgroundFullUser(userToRebind);
             userIds = new IntArray(1);
             userIds.add(userToRebind);
         }
@@ -1488,8 +1471,7 @@
 
             // Filter approvedComponentsByUser to collect all of the components that are allowed
             // for the currently active user(s).
-            populateComponentsToBind(componentsToBind, userIds, approvedComponentsByUser,
-                    isVisibleBackgroundUser);
+            populateComponentsToBind(componentsToBind, userIds, approvedComponentsByUser);
 
             // For every current non-system connection, disconnect services that are no longer
             // approved, or ALL services if we are force rebinding
diff --git a/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
new file mode 100644
index 0000000..8ec7160
--- /dev/null
+++ b/services/core/java/com/android/server/os/instrumentation/DynamicInstrumentationManagerService.java
@@ -0,0 +1,135 @@
+/*
+ * 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.os.instrumentation;
+
+import static android.Manifest.permission.DYNAMIC_INSTRUMENTATION;
+import static android.content.Context.DYNAMIC_INSTRUMENTATION_SERVICE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.PermissionManuallyEnforced;
+import android.content.Context;
+import android.os.instrumentation.ExecutableMethodFileOffsets;
+import android.os.instrumentation.IDynamicInstrumentationManager;
+import android.os.instrumentation.MethodDescriptor;
+import android.os.instrumentation.TargetProcess;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+import dalvik.system.VMDebug;
+
+import java.lang.reflect.Method;
+
+/**
+ * System private implementation of the {@link IDynamicInstrumentationManager interface}.
+ */
+public class DynamicInstrumentationManagerService extends SystemService {
+    public DynamicInstrumentationManagerService(@NonNull Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(DYNAMIC_INSTRUMENTATION_SERVICE, new BinderService());
+    }
+
+    private final class BinderService extends IDynamicInstrumentationManager.Stub {
+        @Override
+        @PermissionManuallyEnforced
+        public @Nullable ExecutableMethodFileOffsets getExecutableMethodFileOffsets(
+                @NonNull TargetProcess targetProcess, @NonNull MethodDescriptor methodDescriptor) {
+            if (!com.android.art.flags.Flags.executableMethodFileOffsets()) {
+                throw new UnsupportedOperationException();
+            }
+            getContext().enforceCallingOrSelfPermission(
+                    DYNAMIC_INSTRUMENTATION, "Caller must have DYNAMIC_INSTRUMENTATION permission");
+
+            if (targetProcess.processName == null
+                    || !targetProcess.processName.equals("system_server")) {
+                throw new UnsupportedOperationException(
+                        "system_server is the only supported target process");
+            }
+
+            Method method = parseMethodDescriptor(
+                    getClass().getClassLoader(), methodDescriptor);
+            VMDebug.ExecutableMethodFileOffsets location =
+                    VMDebug.getExecutableMethodFileOffsets(method);
+
+            if (location == null) {
+                return null;
+            }
+
+            ExecutableMethodFileOffsets ret = new ExecutableMethodFileOffsets();
+            ret.containerPath = location.getContainerPath();
+            ret.containerOffset = location.getContainerOffset();
+            ret.methodOffset = location.getMethodOffset();
+            return ret;
+        }
+    }
+
+    @VisibleForTesting
+    static Method parseMethodDescriptor(ClassLoader classLoader,
+            @NonNull MethodDescriptor descriptor) {
+        try {
+            Class<?> javaClass = classLoader.loadClass(descriptor.fullyQualifiedClassName);
+            Class<?>[] parameters = new Class[descriptor.fullyQualifiedParameters.length];
+            for (int i = 0; i < descriptor.fullyQualifiedParameters.length; i++) {
+                String typeName = descriptor.fullyQualifiedParameters[i];
+                boolean isArrayType = typeName.endsWith("[]");
+                if (isArrayType) {
+                    typeName = typeName.substring(0, typeName.length() - 2);
+                }
+                switch (typeName) {
+                    case "boolean":
+                        parameters[i] = isArrayType ? boolean.class.arrayType() : boolean.class;
+                        break;
+                    case "byte":
+                        parameters[i] = isArrayType ? byte.class.arrayType() : byte.class;
+                        break;
+                    case "char":
+                        parameters[i] = isArrayType ? char.class.arrayType() : char.class;
+                        break;
+                    case "short":
+                        parameters[i] = isArrayType ? short.class.arrayType() : short.class;
+                        break;
+                    case "int":
+                        parameters[i] = isArrayType ? int.class.arrayType() : int.class;
+                        break;
+                    case "long":
+                        parameters[i] = isArrayType ? long.class.arrayType() : long.class;
+                        break;
+                    case "float":
+                        parameters[i] = isArrayType ? float.class.arrayType() : float.class;
+                        break;
+                    case "double":
+                        parameters[i] = isArrayType ? double.class.arrayType() : double.class;
+                        break;
+                    default:
+                        parameters[i] = isArrayType ? classLoader.loadClass(typeName).arrayType()
+                                : classLoader.loadClass(typeName);
+                }
+            }
+
+            return javaClass.getDeclaredMethod(descriptor.methodName, parameters);
+        } catch (ClassNotFoundException | NoSuchMethodException e) {
+            throw new IllegalArgumentException(
+                    "The specified method cannot be found. Is this descriptor valid? "
+                            + descriptor, e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index af2bb17..d538bb8 100644
--- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -265,6 +265,7 @@
 
         @Override
         public void handleMessage(Message msg) {
+            Slog.d(TAG, "Package event received: " + msg.what);
             switch (msg.what) {
                 case MSG_USAGE_EVENT_RECEIVED:
                     mService.handleUsageEvent(
@@ -326,6 +327,8 @@
             return;
         }
 
+        Slog.d(TAG, "handlePackageAdd: adding " + packageName + " from "
+            + userId + " and notifying callbacks");
         initBackgroundInstalledPackages();
         mBackgroundInstalledPackages.add(userId, packageName);
         mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_INSTALL);
@@ -364,7 +367,11 @@
     // ADB sets installerPackageName to null, this creates a loophole to bypass BIC which will be
     // addressed with b/265203007
     private boolean installedByAdb(String initiatingPackageName) {
-        return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName);
+        if(PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName)) {
+            Slog.d(TAG, "handlePackageAdd: is installed by ADB, skipping");
+            return true;
+        }
+        return false;
     }
 
     private boolean wasForegroundInstallation(
@@ -407,6 +414,7 @@
         if (mBackgroundInstalledPackages.contains(userId, packageName)) {
             mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_UNINSTALL);
         }
+        Slog.d(TAG, "handlePackageRemove: removing " + packageName + " from " + userId);
         mBackgroundInstalledPackages.remove(userId, packageName);
         writeBackgroundInstalledPackagesToDisk();
     }
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index a28e3c1..52e8c52 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -38,6 +38,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
@@ -45,7 +46,8 @@
 
 /** Helper class to handle PackageMonitorCallback and notify the registered client. This is mainly
  * used by PackageMonitor to improve the broadcast latency. */
-class PackageMonitorCallbackHelper {
+@VisibleForTesting
+public class PackageMonitorCallbackHelper {
 
     private static final boolean DEBUG = false;
     private static final String TAG = "PackageMonitorCallbackHelper";
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 19406b4..8d039f1 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -83,7 +83,7 @@
 import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED;
 import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled;
 
-import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
+import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
 import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
 import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
 import static com.android.hardware.input.Flags.modifierShortcutDump;
@@ -3993,10 +3993,14 @@
                     return true;
                 }
             case KeyEvent.KEYCODE_SCREENSHOT:
-                if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) {
+                if (firstDown) {
                     interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
                 }
                 return true;
+            case KeyEvent.KEYCODE_DO_NOT_DISTURB:
+            case KeyEvent.KEYCODE_LOCK:
+            case KeyEvent.KEYCODE_FULLSCREEN:
+                return true;
         }
         if (isValidGlobalKey(keyCode)
                 && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -5666,9 +5670,23 @@
             case KeyEvent.KEYCODE_MACRO_4:
                 result &= ~ACTION_PASS_TO_USER;
                 break;
-            case KeyEvent.KEYCODE_EMOJI_PICKER:
-                if (!emojiAndScreenshotKeycodesAvailable()) {
-                    // Don't allow EMOJI_PICKER key to be dispatched until flag is released.
+            case KeyEvent.KEYCODE_DICTATE:
+            case KeyEvent.KEYCODE_NEW:
+            case KeyEvent.KEYCODE_CLOSE:
+            case KeyEvent.KEYCODE_PRINT:
+            case KeyEvent.KEYCODE_F13:
+            case KeyEvent.KEYCODE_F14:
+            case KeyEvent.KEYCODE_F15:
+            case KeyEvent.KEYCODE_F16:
+            case KeyEvent.KEYCODE_F17:
+            case KeyEvent.KEYCODE_F18:
+            case KeyEvent.KEYCODE_F19:
+            case KeyEvent.KEYCODE_F20:
+            case KeyEvent.KEYCODE_F21:
+            case KeyEvent.KEYCODE_F22:
+            case KeyEvent.KEYCODE_F23:
+            case KeyEvent.KEYCODE_F24:
+                if (!enableNew25q2Keycodes()) {
                     result &= ~ACTION_PASS_TO_USER;
                 }
                 break;
@@ -5841,8 +5859,8 @@
     public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
             long whenNanos, int policyFlags) {
         if ((policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(
-                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
+                    action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -5856,8 +5874,8 @@
         // there will be no dream to intercept the touch and wake into ambient.  The device should
         // wake up in this case.
         if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(
-                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
+                    action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -6205,7 +6223,7 @@
     }
 
     private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) {
-        if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) {
+        if (mWindowWakeUpPolicy.wakeUpFromKey(DEFAULT_DISPLAY, eventTime, keyCode, isDown)) {
             final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER;
             // Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout
             if (shouldWakeUpWithHomeIntent() &&  keyCanLaunchHome) {
diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
index af1ad13..04dbd1f 100644
--- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
@@ -25,6 +25,7 @@
 import static android.view.KeyEvent.KEYCODE_POWER;
 
 import static com.android.server.policy.Flags.supportInputWakeupDelegate;
+import static com.android.server.power.feature.flags.Flags.perDisplayWakeByTouch;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -107,13 +108,14 @@
     /**
      * Wakes up from a key event.
      *
+     * @param displayId the id of the display to wake.
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
      * @param keyCode the {@link android.view.KeyEvent} key code of the key event.
      * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) {
+    boolean wakeUpFromKey(int displayId, long eventTime, int keyCode, boolean isDown) {
         final boolean wakeAllowedDuringTheaterMode =
                 keyCode == KEYCODE_POWER
                         ? mAllowTheaterModeWakeFromPowerKey
@@ -126,22 +128,31 @@
                 && mInputWakeUpDelegate.wakeUpFromKey(eventTime, keyCode, isDown)) {
             return true;
         }
-        wakeUp(
-                eventTime,
-                keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
-                keyCode == KEYCODE_POWER ? "POWER" : "KEY");
+        if (perDisplayWakeByTouch()) {
+            wakeUp(
+                    displayId,
+                    eventTime,
+                    keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
+                    keyCode == KEYCODE_POWER ? "POWER" : "KEY");
+        } else {
+            wakeUp(
+                    eventTime,
+                    keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
+                    keyCode == KEYCODE_POWER ? "POWER" : "KEY");
+        }
         return true;
     }
 
     /**
      * Wakes up from a motion event.
      *
+     * @param displayId the id of the display to wake.
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
      * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) {
+    boolean wakeUpFromMotion(int displayId, long eventTime, int source, boolean isDown) {
         if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) {
             if (DEBUG) Slog.d(TAG, "Unable to wake up from motion.");
             return false;
@@ -150,7 +161,11 @@
                 && mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) {
             return true;
         }
-        wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+        if (perDisplayWakeByTouch()) {
+            wakeUp(displayId, eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+        } else {
+            wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+        }
         return true;
     }
 
@@ -237,4 +252,12 @@
     private void wakeUp(long wakeTime, @WakeReason int reason, String details) {
         mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
     }
+
+    /** Wakes up given display. */
+    private void wakeUp(int displayId, long wakeTime, @WakeReason int reason, String details) {
+        // If we're given an invalid display id to wake, fall back to waking default display
+        final int displayIdToWake =
+                displayId == Display.INVALID_DISPLAY ? Display.DEFAULT_DISPLAY : displayId;
+        mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details, displayIdToWake);
+    }
 }
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 677a2de..028ac57 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -303,6 +303,8 @@
     private final GnssPowerStatsCollector mGnssPowerStatsCollector;
     private final CustomEnergyConsumerPowerStatsCollector mCustomEnergyConsumerPowerStatsCollector;
     private final SparseBooleanArray mPowerStatsCollectorEnabled = new SparseBooleanArray();
+    private boolean mMoveWscLoggingToNotifierEnabled = false;
+
     private ScreenPowerStatsCollector.ScreenUsageTimeRetriever mScreenUsageTimeRetriever =
             new ScreenPowerStatsCollector.ScreenUsageTimeRetriever() {
 
@@ -5155,7 +5157,7 @@
 
             Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
             uidStats.noteStartWakeLocked(pid, name, type, elapsedRealtimeMs);
-            if (!mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()) {
+            if (!mMoveWscLoggingToNotifierEnabled) {
                 mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name,
                         uidStats.mProcessState, true /* acquired */,
                         getPowerManagerWakeLockLevel(type));
@@ -5206,7 +5208,7 @@
             Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
             uidStats.noteStopWakeLocked(pid, name, type, elapsedRealtimeMs);
 
-            if (!mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()) {
+            if (!mMoveWscLoggingToNotifierEnabled) {
                 mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name,
                         uidStats.mProcessState, false/* acquired */,
                         getPowerManagerWakeLockLevel(type));
@@ -15975,6 +15977,15 @@
         }
     }
 
+    /**
+     * Controls where the logging of the WakelockStateChanged atom occurs:
+     *   true = Notifier, false = BatteryStatsImpl.
+     */
+    public void setMoveWscLoggingToNotifierEnabled(boolean enabled) {
+        synchronized (this) {
+            mMoveWscLoggingToNotifierEnabled = enabled;
+        }
+    }
     @GuardedBy("this")
     public void systemServicesReady(Context context) {
         mConstants.startObserving(context.getContentResolver());
diff --git a/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java b/services/core/java/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationService.java
similarity index 96%
rename from services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java
rename to services/core/java/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationService.java
index 9398c7a..b129fdc 100644
--- a/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java
+++ b/services/core/java/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationService.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.adaptiveauth;
+package com.android.server.security.adaptiveauthentication;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
 
@@ -55,8 +55,8 @@
 /**
  * @hide
  */
-public class AdaptiveAuthService extends SystemService {
-    private static final String TAG = "AdaptiveAuthService";
+public class AdaptiveAuthenticationService extends SystemService {
+    private static final String TAG = "AdaptiveAuthenticationService";
     private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
 
     @VisibleForTesting
@@ -78,12 +78,12 @@
     final SparseIntArray mFailedAttemptsForUser = new SparseIntArray();
     private final SparseLongArray mLastLockedTimestamp = new SparseLongArray();
 
-    public AdaptiveAuthService(Context context) {
+    public AdaptiveAuthenticationService(Context context) {
         this(context, new LockPatternUtils(context));
     }
 
     @VisibleForTesting
-    public AdaptiveAuthService(Context context, LockPatternUtils lockPatternUtils) {
+    public AdaptiveAuthenticationService(Context context, LockPatternUtils lockPatternUtils) {
         super(context);
         mLockPatternUtils = lockPatternUtils;
         mLockSettings = Objects.requireNonNull(
diff --git a/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS b/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
new file mode 100644
index 0000000..29affcd
--- /dev/null
+++ b/services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
@@ -0,0 +1,3 @@
+hainingc@google.com
+jbolinger@google.com
+graciecheng@google.com
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index df44e50..a92ac67 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -45,6 +45,7 @@
         void onExternalVibrationReleased(long vibrationId);
     }
 
+    private final long mSessionId = VibrationSession.nextSessionId();
     private final ExternalVibration mExternalVibration;
     private final ExternalVibrationScale mScale = new ExternalVibrationScale();
     private final VibratorManagerHooks mManagerHooks;
@@ -65,6 +66,11 @@
     }
 
     @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
     public long getCreateUptimeMillis() {
         return stats.getCreateUptimeMillis();
     }
@@ -148,7 +154,12 @@
 
     @Override
     public void notifySyncedVibratorsCallback(long vibrationId) {
-        // ignored, external control does not expect callbacks from the vibrator manager
+        // ignored, external control does not expect callbacks from the vibrator manager for sync
+    }
+
+    @Override
+    public void notifySessionCallback() {
+        // ignored, external control does not expect callbacks from the vibrator manager for session
     }
 
     boolean isHoldingSameVibration(ExternalVibration vib) {
@@ -174,7 +185,8 @@
     @Override
     public String toString() {
         return "ExternalVibrationSession{"
-                + "id=" + id
+                + "sessionId=" + mSessionId
+                + ", vibrationId=" + id
                 + ", callerInfo=" + callerInfo
                 + ", externalVibration=" + mExternalVibration
                 + ", scale=" + mScale
diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
index 67ba25f..628221b 100644
--- a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java
@@ -35,6 +35,7 @@
     private static final String TAG = "SingleVibrationSession";
 
     private final Object mLock = new Object();
+    private final long mSessionId = VibrationSession.nextSessionId();
     private final IBinder mCallerToken;
     private final HalVibration mVibration;
 
@@ -58,6 +59,11 @@
     }
 
     @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
     public long getCreateUptimeMillis() {
         return mVibration.stats.getCreateUptimeMillis();
     }
@@ -155,9 +161,15 @@
     }
 
     @Override
+    public void notifySessionCallback() {
+        // ignored, external control does not expect callbacks from the vibrator manager for session
+    }
+
+    @Override
     public String toString() {
         return "SingleVibrationSession{"
-                + "callerToken= " + mCallerToken
+                + "sessionId= " + mSessionId
+                + ", callerToken= " + mCallerToken
                 + ", vibration=" + mVibration
                 + '}';
     }
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
new file mode 100644
index 0000000..07478e3
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -0,0 +1,493 @@
+/*
+ * 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.vibrator;
+
+import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioAttributes;
+import android.os.CancellationSignal;
+import android.os.CombinedVibration;
+import android.os.ExternalVibration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.VibrationAttributes;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
+import android.util.IndentingPrintWriter;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+/**
+ * A vibration session started by a vendor request that can trigger {@link CombinedVibration}.
+ */
+final class VendorVibrationSession extends IVibrationSession.Stub
+        implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
+    private static final String TAG = "VendorVibrationSession";
+
+    /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
+    interface VibratorManagerHooks {
+
+        /** Tells the manager to end the vibration session. */
+        void endSession(long sessionId, boolean shouldAbort);
+
+        /**
+         * Tells the manager that the vibration session is finished and the vibrators can now be
+         * used for another vibration.
+         */
+        void onSessionReleased(long sessionId);
+    }
+
+    private final Object mLock = new Object();
+    private final long mSessionId = VibrationSession.nextSessionId();
+    private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
+    private final int[] mVibratorIds;
+    private final long mCreateUptime;
+    private final long mCreateTime; // for debugging
+    private final IVibrationSessionCallback mCallback;
+    private final CallerInfo mCallerInfo;
+    private final VibratorManagerHooks mManagerHooks;
+    private final Handler mHandler;
+
+    @GuardedBy("mLock")
+    private Status mStatus = Status.RUNNING;
+    @GuardedBy("mLock")
+    private Status mEndStatusRequest;
+    @GuardedBy("mLock")
+    private long mStartTime; // for debugging
+    @GuardedBy("mLock")
+    private long mEndUptime;
+    @GuardedBy("mLock")
+    private long mEndTime; // for debugging
+
+    VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler,
+            @NonNull VibratorManagerHooks managerHooks, @NonNull int[] vibratorIds,
+            @NonNull IVibrationSessionCallback callback) {
+        mCreateUptime = SystemClock.uptimeMillis();
+        mCreateTime = System.currentTimeMillis();
+        mVibratorIds = vibratorIds;
+        mHandler = handler;
+        mCallback = callback;
+        mCallerInfo = callerInfo;
+        mManagerHooks = managerHooks;
+        CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this);
+    }
+
+    @Override
+    public void vibrate(CombinedVibration vibration, String reason) {
+        // TODO(b/345414356): implement vibration support
+        throw new UnsupportedOperationException("Vendor session vibrations not yet implemented");
+    }
+
+    @Override
+    public void finishSession() {
+        // Do not abort session in HAL, wait for ongoing vibration requests to complete.
+        // This might take a while to end the session, but it can be aborted by cancelSession.
+        requestEndSession(Status.FINISHED, /* shouldAbort= */ false);
+    }
+
+    @Override
+    public void cancelSession() {
+        // Always abort session in HAL while cancelling it.
+        // This might be triggered after finishSession was already called.
+        requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true);
+    }
+
+    @Override
+    public long getSessionId() {
+        return mSessionId;
+    }
+
+    @Override
+    public long getCreateUptimeMillis() {
+        return mCreateUptime;
+    }
+
+    @Override
+    public boolean isRepeating() {
+        return false;
+    }
+
+    @Override
+    public CallerInfo getCallerInfo() {
+        return mCallerInfo;
+    }
+
+    @Override
+    public IBinder getCallerToken() {
+        return mCallback.asBinder();
+    }
+
+    @Override
+    public DebugInfo getDebugInfo() {
+        synchronized (mLock) {
+            return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime,
+                    mEndUptime, mEndTime);
+        }
+    }
+
+    @Override
+    public boolean wasEndRequested() {
+        synchronized (mLock) {
+            return mEndStatusRequest != null;
+        }
+    }
+
+    @Override
+    public void onCancel() {
+        Slog.d(TAG, "Cancellation signal received, cancelling vibration session...");
+        requestEnd(Status.CANCELLED_BY_USER, /* endedBy= */ null, /* immediate= */ false);
+    }
+
+    @Override
+    public void binderDied() {
+        Slog.d(TAG, "Binder died, cancelling vibration session...");
+        requestEnd(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* immediate= */ false);
+    }
+
+    @Override
+    public boolean linkToDeath() {
+        try {
+            mCallback.asBinder().linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error linking session to token death", e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void unlinkToDeath() {
+        try {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            Slog.wtf(TAG, "Failed to unlink session to token death", e);
+        }
+    }
+
+    @Override
+    public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+            boolean immediate) {
+        // All requests to end a session should abort it to stop ongoing vibrations, even if
+        // immediate flag is false. Only the #finishSession API will not abort and wait for
+        // session vibrations to complete, which might take a long time.
+        requestEndSession(status, /* shouldAbort= */ true);
+    }
+
+    @Override
+    public void notifyVibratorCallback(int vibratorId, long vibrationId) {
+        // TODO(b/345414356): implement vibration support
+    }
+
+    @Override
+    public void notifySyncedVibratorsCallback(long vibrationId) {
+        // TODO(b/345414356): implement vibration support
+    }
+
+    @Override
+    public void notifySessionCallback() {
+        synchronized (mLock) {
+            // If end was not requested then the HAL has cancelled the session.
+            maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON);
+            maybeSetStatusToRequestedLocked();
+        }
+        mManagerHooks.onSessionReleased(mSessionId);
+    }
+
+    @Override
+    public String toString() {
+        synchronized (mLock) {
+            return "createTime: " + formatTime(mCreateTime, /*includeDate=*/ true)
+                    + ", startTime: " + (mStartTime == 0 ? null : formatTime(mStartTime,
+                    /* includeDate= */ true))
+                    + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime,
+                    /* includeDate= */ true))
+                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
+                    + ", callerInfo: " + mCallerInfo
+                    + ", vibratorIds: " + Arrays.toString(mVibratorIds);
+        }
+    }
+
+    public Status getStatus() {
+        synchronized (mLock) {
+            return mStatus;
+        }
+    }
+
+    public boolean isStarted() {
+        synchronized (mLock) {
+            return mStartTime > 0;
+        }
+    }
+
+    public boolean isEnded() {
+        synchronized (mLock) {
+            return mStatus != Status.RUNNING;
+        }
+    }
+
+    public int[] getVibratorIds() {
+        return mVibratorIds;
+    }
+
+    public ICancellationSignal getCancellationSignal() {
+        return mCancellationSignal;
+    }
+
+    public void notifyStart() {
+        boolean isAlreadyEnded = false;
+        synchronized (mLock) {
+            if (isEnded()) {
+                // Session already ended, skip start callbacks.
+                isAlreadyEnded = true;
+            } else {
+                mStartTime = System.currentTimeMillis();
+                // Run client callback in separate thread.
+                mHandler.post(() -> {
+                    try {
+                        mCallback.onStarted(this);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error notifying vendor session started", e);
+                    }
+                });
+            }
+        }
+        if (isAlreadyEnded) {
+            // Session already ended, make sure we end it in the HAL.
+            mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true);
+        }
+    }
+
+    private void requestEndSession(Status status, boolean shouldAbort) {
+        boolean shouldTriggerSessionHook = false;
+        synchronized (mLock) {
+            maybeSetEndRequestLocked(status);
+            if (isStarted()) {
+                // Always trigger session hook after it has started, in case new request aborts an
+                // already finishing session. Wait for HAL callback before actually ending here.
+                shouldTriggerSessionHook = true;
+            } else {
+                // Session did not start in the HAL, end it right away.
+                maybeSetStatusToRequestedLocked();
+            }
+        }
+        if (shouldTriggerSessionHook) {
+            mManagerHooks.endSession(mSessionId, shouldAbort);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void maybeSetEndRequestLocked(Status status) {
+        if (mEndStatusRequest != null) {
+            // End already requested, keep first requested status and time.
+            return;
+        }
+        mEndStatusRequest = status;
+        mEndTime = System.currentTimeMillis();
+        mEndUptime = SystemClock.uptimeMillis();
+        if (isStarted()) {
+            // Only trigger "finishing" callback if session started.
+            // Run client callback in separate thread.
+            mHandler.post(() -> {
+                try {
+                    mCallback.onFinishing();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error notifying vendor session is finishing", e);
+                }
+            });
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void maybeSetStatusToRequestedLocked() {
+        if (isEnded()) {
+            // End already set, keep first requested status and time.
+            return;
+        }
+        if (mEndStatusRequest == null) {
+            // No end status was requested, nothing to set.
+            return;
+        }
+        mStatus = mEndStatusRequest;
+        // Run client callback in separate thread.
+        final Status endStatus = mStatus;
+        mHandler.post(() -> {
+            try {
+                mCallback.onFinished(toSessionStatus(endStatus));
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error notifying vendor session is finishing", e);
+            }
+        });
+    }
+
+    @android.os.vibrator.VendorVibrationSession.Status
+    private static int toSessionStatus(Status status) {
+        // Exhaustive switch to cover all possible internal status.
+        return switch (status) {
+            case FINISHED
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+            case IGNORED_UNSUPPORTED
+                    -> STATUS_UNSUPPORTED;
+            case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+                 CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+                 CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+            case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+                 IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+                 IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+                 IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+            case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
+                 IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
+                    -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+        };
+    }
+
+    /**
+     * Holds lightweight debug information about the session that could potentially be kept in
+     * memory for a long time for bugreport dumpsys operations.
+     *
+     * Since DebugInfo can be kept in memory for a long time, it shouldn't hold any references to
+     * potentially expensive or resource-linked objects, such as {@link IBinder}.
+     */
+    static final class DebugInfoImpl implements VibrationSession.DebugInfo {
+        private final Status mStatus;
+        private final CallerInfo mCallerInfo;
+
+        private final long mCreateUptime;
+        private final long mCreateTime;
+        private final long mStartTime;
+        private final long mEndTime;
+        private final long mDurationMs;
+
+        DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime,
+                long startTime, long endUptime, long endTime) {
+            mStatus = status;
+            mCallerInfo = callerInfo;
+            mCreateUptime = createUptime;
+            mCreateTime = createTime;
+            mStartTime = startTime;
+            mEndTime = endTime;
+            mDurationMs = endUptime > 0 ? endUptime - createUptime : -1;
+        }
+
+        @Override
+        public Status getStatus() {
+            return mStatus;
+        }
+
+        @Override
+        public long getCreateUptimeMillis() {
+            return mCreateUptime;
+        }
+
+        @Override
+        public CallerInfo getCallerInfo() {
+            return mCallerInfo;
+        }
+
+        @Nullable
+        @Override
+        public Object getDumpAggregationKey() {
+            return null; // No aggregation.
+        }
+
+        @Override
+        public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
+        }
+
+        @Override
+        public void dump(ProtoOutputStream proto, long fieldId) {
+            final long token = proto.start(fieldId);
+            proto.write(VibrationProto.END_TIME, mEndTime);
+            proto.write(VibrationProto.DURATION_MS, mDurationMs);
+            proto.write(VibrationProto.STATUS, mStatus.ordinal());
+
+            final long attrsToken = proto.start(VibrationProto.ATTRIBUTES);
+            final VibrationAttributes attrs = mCallerInfo.attrs;
+            proto.write(VibrationAttributesProto.USAGE, attrs.getUsage());
+            proto.write(VibrationAttributesProto.AUDIO_USAGE, attrs.getAudioUsage());
+            proto.write(VibrationAttributesProto.FLAGS, attrs.getFlags());
+            proto.end(attrsToken);
+
+            proto.end(token);
+        }
+
+        @Override
+        public void dump(IndentingPrintWriter pw) {
+            pw.println("VibrationSession:");
+            pw.increaseIndent();
+            pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT));
+            pw.println("durationMs = " + mDurationMs);
+            pw.println("createTime = " + formatTime(mCreateTime, /*includeDate=*/ true));
+            pw.println("startTime = " + formatTime(mStartTime, /*includeDate=*/ true));
+            pw.println("endTime = " + (mEndTime == 0 ? null
+                    : formatTime(mEndTime, /*includeDate=*/ true)));
+            pw.println("callerInfo = " + mCallerInfo);
+            pw.decreaseIndent();
+        }
+
+        @Override
+        public void dumpCompact(IndentingPrintWriter pw) {
+            // Follow pattern from Vibration.DebugInfoImpl for better debugging from dumpsys.
+            String timingsStr = String.format(Locale.ROOT,
+                    "%s | %8s | %20s | duration: %5dms | start: %12s | end: %12s",
+                    formatTime(mCreateTime, /*includeDate=*/ true),
+                    "session",
+                    mStatus.name().toLowerCase(Locale.ROOT),
+                    mDurationMs,
+                    mStartTime == 0 ? "" : formatTime(mStartTime, /*includeDate=*/ false),
+                    mEndTime == 0 ? "" : formatTime(mEndTime, /*includeDate=*/ false));
+            String paramStr = String.format(Locale.ROOT,
+                    " | flags: %4s | usage: %s",
+                    Long.toBinaryString(mCallerInfo.attrs.getFlags()),
+                    mCallerInfo.attrs.usageToString());
+            // Optional, most vibrations should not be defined via AudioAttributes
+            // so skip them to simplify the logs
+            String audioUsageStr =
+                    mCallerInfo.attrs.getOriginalAudioUsage() != AudioAttributes.USAGE_UNKNOWN
+                            ? " | audioUsage=" + AudioAttributes.usageToString(
+                            mCallerInfo.attrs.getOriginalAudioUsage())
+                            : "";
+            String callerStr = String.format(Locale.ROOT,
+                    " | %s (uid=%d, deviceId=%d) | reason: %s",
+                    mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason);
+            pw.println(timingsStr + paramStr + audioUsageStr + callerStr);
+        }
+
+        @Override
+        public String toString() {
+            return "createTime: " + formatTime(mCreateTime, /* includeDate= */ true)
+                    + ", startTime: " + formatTime(mStartTime, /* includeDate= */ true)
+                    + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime,
+                    /* includeDate= */ true))
+                    + ", durationMs: " + mDurationMs
+                    + ", status: " + mStatus.name().toLowerCase(Locale.ROOT)
+                    + ", callerInfo: " + mCallerInfo;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index bb2a17c..27f92b2 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vibrator;
 
+import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.media.AudioAttributes;
@@ -31,9 +33,6 @@
 import android.util.IndentingPrintWriter;
 import android.util.proto.ProtoOutputStream;
 
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -42,11 +41,6 @@
  * The base class for all vibrations.
  */
 abstract class Vibration {
-    private static final DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern(
-            "HH:mm:ss.SSS");
-    private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
-            "MM-dd HH:mm:ss.SSS");
-
     // Used to generate globally unique vibration ids.
     private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback
 
@@ -399,12 +393,5 @@
             proto.write(PrimitiveSegmentProto.DELAY, segment.getDelay());
             proto.end(token);
         }
-
-        private String formatTime(long timeInMillis, boolean includeDate) {
-            return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER)
-                    // Ensure timezone is retrieved at formatting time
-                    .withZone(ZoneId.systemDefault())
-                    .format(Instant.ofEpochMilli(timeInMillis));
-        }
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index b511ba8..ae95a70 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -25,7 +25,11 @@
 import android.util.proto.ProtoOutputStream;
 
 import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Represents a generic vibration session that plays one or more vibration requests.
@@ -39,6 +43,16 @@
  */
 interface VibrationSession {
 
+    // Used to generate globally unique session ids.
+    AtomicInteger sNextSessionId = new AtomicInteger(1); // 0 = no callback
+
+    static long nextSessionId() {
+        return sNextSessionId.getAndIncrement();
+    }
+
+    /** Returns the session id. */
+    long getSessionId();
+
     /** Returns the session creation time from {@link android.os.SystemClock#uptimeMillis()}. */
     long getCreateUptimeMillis();
 
@@ -105,6 +119,14 @@
     void notifySyncedVibratorsCallback(long vibrationId);
 
     /**
+     * Notify vibrator manager have completed the vibration session.
+     *
+     * <p>This will be called by the vibrator manager hardware callback indicating the session
+     * is complete, either because it was ended or cancelled by the service or the vendor.
+     */
+    void notifySessionCallback();
+
+    /**
      * Session status with reference to values from vibratormanagerservice.proto for logging.
      */
     enum Status {
@@ -212,6 +234,17 @@
      */
     interface DebugInfo {
 
+        DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+        DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
+                "MM-dd HH:mm:ss.SSS");
+
+        static String formatTime(long timeInMillis, boolean includeDate) {
+            return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER)
+                    // Ensure timezone is retrieved at formatting time
+                    .withZone(ZoneId.systemDefault())
+                    .format(Instant.ofEpochMilli(timeInMillis));
+        }
+
         /** Return the vibration session status. */
         Status getStatus();
 
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index ff34911..4764481 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -32,6 +32,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.hardware.vibrator.IVibrator;
+import android.hardware.vibrator.IVibratorManager;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Build;
@@ -40,6 +41,7 @@
 import android.os.ExternalVibrationScale;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.ICancellationSignal;
 import android.os.IExternalVibratorService;
 import android.os.IVibratorManagerService;
 import android.os.IVibratorStateListener;
@@ -57,6 +59,7 @@
 import android.os.VibrationEffect;
 import android.os.VibratorInfo;
 import android.os.vibrator.Flags;
+import android.os.vibrator.IVibrationSessionCallback;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
@@ -103,7 +106,7 @@
     private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service";
     private static final String VIBRATOR_CONTROL_SERVICE =
             "android.frameworks.vibrator.IVibratorControlService/default";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private static final VibrationAttributes DEFAULT_ATTRIBUTES =
             new VibrationAttributes.Builder().build();
     private static final int ATTRIBUTES_ALL_BYPASS_FLAGS =
@@ -159,12 +162,14 @@
             new VibrationThreadCallbacks();
     private final ExternalVibrationCallbacks mExternalVibrationCallbacks =
             new ExternalVibrationCallbacks();
+    private final VendorVibrationSessionCallbacks mVendorVibrationSessionCallbacks =
+            new VendorVibrationSessionCallbacks();
     @GuardedBy("mLock")
     private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>();
     @GuardedBy("mLock")
-    private VibrationSession mCurrentVibration;
+    private VibrationSession mCurrentSession;
     @GuardedBy("mLock")
-    private VibrationSession mNextVibration;
+    private VibrationSession mNextSession;
     @GuardedBy("mLock")
     private boolean mServiceReady;
 
@@ -191,14 +196,14 @@
                 // When the system is entering a non-interactive state, we want to cancel
                 // vibrations in case a misbehaving app has abandoned them.
                 synchronized (mLock) {
-                    maybeClearCurrentAndNextVibrationsLocked(
+                    maybeClearCurrentAndNextSessionsLocked(
                             VibratorManagerService.this::shouldCancelOnScreenOffLocked,
                             Status.CANCELLED_BY_SCREEN_OFF);
                 }
             } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()
                     && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) {
                 synchronized (mLock) {
-                    maybeClearCurrentAndNextVibrationsLocked(
+                    maybeClearCurrentAndNextSessionsLocked(
                             VibratorManagerService.this::shouldCancelOnFgUserRequest,
                             Status.CANCELLED_BY_FOREGROUND_USER);
                 }
@@ -215,14 +220,14 @@
                         return;
                     }
                     synchronized (mLock) {
-                        maybeClearCurrentAndNextVibrationsLocked(
+                        maybeClearCurrentAndNextSessionsLocked(
                                 VibratorManagerService.this::shouldCancelAppOpModeChangedLocked,
                                 Status.CANCELLED_BY_APP_OPS);
                     }
                 }
             };
 
-    static native long nativeInit(OnSyncedVibrationCompleteListener listener);
+    static native long nativeInit(VibratorManagerNativeCallbacks listener);
 
     static native long nativeGetFinalizer();
 
@@ -236,6 +241,13 @@
 
     static native void nativeCancelSynced(long nativeServicePtr);
 
+    static native boolean nativeStartSession(long nativeServicePtr, long sessionId,
+            int[] vibratorIds);
+
+    static native void nativeEndSession(long nativeServicePtr, long sessionId, boolean shouldAbort);
+
+    static native void nativeClearSessions(long nativeServicePtr);
+
     @VisibleForTesting
     VibratorManagerService(Context context, Injector injector) {
         mContext = context;
@@ -303,6 +315,9 @@
         // Reset the hardware to a default state, in case this is a runtime restart instead of a
         // fresh boot.
         mNativeWrapper.cancelSynced();
+        if (Flags.vendorVibrationEffects()) {
+            mNativeWrapper.clearSessions();
+        }
         for (int i = 0; i < mVibrators.size(); i++) {
             mVibrators.valueAt(i).reset();
         }
@@ -363,6 +378,11 @@
     }
 
     @Override // Binder call
+    public int getCapabilities() {
+        return (int) mCapabilities;
+    }
+
+    @Override // Binder call
     @Nullable
     public VibratorInfo getVibratorInfo(int vibratorId) {
         final VibratorController controller = mVibrators.get(vibratorId);
@@ -590,11 +610,17 @@
             logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_ERROR_TOKEN);
             return null;
         }
-        if (effect.hasVendorEffects()
-                && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) {
-            Slog.e(TAG, "vibrate; no permission for vendor effects");
-            logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION);
-            return null;
+        if (effect.hasVendorEffects()) {
+            if (!Flags.vendorVibrationEffects()) {
+                Slog.e(TAG, "vibrate; vendor effects feature disabled");
+                logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_UNSUPPORTED);
+                return null;
+            }
+            if (!hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) {
+                Slog.e(TAG, "vibrate; no permission for vendor effects");
+                logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION);
+                return null;
+            }
         }
         enforceUpdateAppOpsStatsPermission(uid);
         if (!isEffectValid(effect)) {
@@ -623,7 +649,7 @@
 
             // Check if ongoing vibration is more important than this vibration.
             if (ignoreStatus == null) {
-                Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(session);
+                Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session);
                 if (vibrationEndInfo != null) {
                     ignoreStatus = vibrationEndInfo.status;
                     ignoredBy = vibrationEndInfo.endedBy;
@@ -634,8 +660,8 @@
             if (ignoreStatus == null) {
                 final long ident = Binder.clearCallingIdentity();
                 try {
-                    if (mCurrentVibration != null) {
-                        if (shouldPipelineVibrationLocked(mCurrentVibration, vib)) {
+                    if (mCurrentSession != null) {
+                        if (shouldPipelineVibrationLocked(mCurrentSession, vib)) {
                             // Don't cancel the current vibration if it's pipeline-able.
                             // Note that if there is a pending next vibration that can't be
                             // pipelined, it will have already cancelled the current one, so we
@@ -645,12 +671,12 @@
                             }
                         } else {
                             vib.stats.reportInterruptedAnotherVibration(
-                                    mCurrentVibration.getCallerInfo());
-                            mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
+                                    mCurrentSession.getCallerInfo());
+                            mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
                                     /* immediate= */ false);
                         }
                     }
-                    clearNextVibrationLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
+                    clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
                     ignoreStatus = startVibrationLocked(session);
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -659,7 +685,7 @@
 
             // Ignored or failed to start the vibration, end it and report metrics right away.
             if (ignoreStatus != null) {
-                endVibrationLocked(session, ignoreStatus, ignoredBy);
+                endSessionLocked(session, ignoreStatus, ignoredBy);
             }
             return vib;
         }
@@ -681,14 +707,14 @@
                 try {
                     // TODO(b/370948466): investigate why token not checked on external vibrations.
                     IBinder cancelToken =
-                            (mNextVibration instanceof ExternalVibrationSession) ? null : token;
-                    if (shouldCancelVibration(mNextVibration, usageFilter, cancelToken)) {
-                        clearNextVibrationLocked(Status.CANCELLED_BY_USER);
+                            (mNextSession instanceof ExternalVibrationSession) ? null : token;
+                    if (shouldCancelSession(mNextSession, usageFilter, cancelToken)) {
+                        clearNextSessionLocked(Status.CANCELLED_BY_USER);
                     }
                     cancelToken =
-                            (mCurrentVibration instanceof ExternalVibrationSession) ? null : token;
-                    if (shouldCancelVibration(mCurrentVibration, usageFilter, cancelToken)) {
-                        mCurrentVibration.requestEnd(Status.CANCELLED_BY_USER);
+                            (mCurrentSession instanceof ExternalVibrationSession) ? null : token;
+                    if (shouldCancelSession(mCurrentSession, usageFilter, cancelToken)) {
+                        mCurrentSession.requestEnd(Status.CANCELLED_BY_USER);
                     }
                 } finally {
                     Binder.restoreCallingIdentity(ident);
@@ -699,6 +725,141 @@
         }
     }
 
+    @android.annotation.EnforcePermission(allOf = {
+            android.Manifest.permission.VIBRATE,
+            android.Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+            android.Manifest.permission.START_VIBRATION_SESSIONS,
+    })
+    @Override // Binder call
+    public ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg,
+            int[] vibratorIds, VibrationAttributes attrs, String reason,
+            IVibrationSessionCallback callback) {
+        startVendorVibrationSession_enforcePermission();
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationSession");
+        try {
+            VendorVibrationSession session = startVendorVibrationSessionInternal(
+                    uid, deviceId, opPkg, vibratorIds, attrs, reason, callback);
+            return session == null ? null : session.getCancellationSignal();
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
+    }
+
+    VendorVibrationSession startVendorVibrationSessionInternal(int uid, int deviceId, String opPkg,
+            int[] vibratorIds, VibrationAttributes attrs, String reason,
+            IVibrationSessionCallback callback) {
+        if (!Flags.vendorVibrationEffects()) {
+            throw new UnsupportedOperationException("Vibration sessions not supported");
+        }
+        attrs = fixupVibrationAttributes(attrs, /* effect= */ null);
+        CallerInfo callerInfo = new CallerInfo(attrs, uid, deviceId, opPkg, reason);
+        if (callback == null) {
+            Slog.e(TAG, "session callback must not be null");
+            logAndRecordSessionAttempt(callerInfo, Status.IGNORED_ERROR_TOKEN);
+            return null;
+        }
+        if (vibratorIds == null) {
+            vibratorIds = new int[0];
+        }
+        enforceUpdateAppOpsStatsPermission(uid);
+        VendorVibrationSession session = new VendorVibrationSession(callerInfo, mHandler,
+                mVendorVibrationSessionCallbacks, vibratorIds, callback);
+
+        if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
+            // Force update of user settings before checking if this vibration effect should
+            // be ignored or scaled.
+            mVibrationSettings.update();
+        }
+
+        synchronized (mLock) {
+            if (DEBUG) {
+                Slog.d(TAG, "Starting session " + session.getSessionId());
+            }
+
+            Status ignoreStatus = null;
+            CallerInfo ignoredBy = null;
+
+            // Check if HAL has capability to start sessions.
+            if ((mCapabilities & IVibratorManager.CAP_START_SESSIONS) == 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Missing capability to start sessions, ignoring request");
+                }
+                ignoreStatus = Status.IGNORED_UNSUPPORTED;
+            }
+
+            // Check if any vibrator ID was requested.
+            if (ignoreStatus == null && vibratorIds.length == 0) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Empty vibrator ids to start session, ignoring request");
+                }
+                ignoreStatus = Status.IGNORED_UNSUPPORTED;
+            }
+
+            // Check if user settings or DnD is set to ignore this session.
+            if (ignoreStatus == null) {
+                ignoreStatus = shouldIgnoreVibrationLocked(callerInfo);
+            }
+
+            // Check if ongoing vibration is more important than this session.
+            if (ignoreStatus == null) {
+                Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session);
+                if (vibrationEndInfo != null) {
+                    ignoreStatus = vibrationEndInfo.status;
+                    ignoredBy = vibrationEndInfo.endedBy;
+                }
+            }
+
+            if (ignoreStatus == null) {
+                final long ident = Binder.clearCallingIdentity();
+                try {
+                    // If not ignored so far then stop ongoing sessions before starting this one.
+                    clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo);
+                    if (mCurrentSession != null) {
+                        mNextSession = session;
+                        mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo,
+                                /* immediate= */ false);
+                    } else {
+                        ignoreStatus = startVendorSessionLocked(session);
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+
+            // Ignored or failed to start the session, end it and report metrics right away.
+            if (ignoreStatus != null) {
+                endSessionLocked(session, ignoreStatus, ignoredBy);
+            }
+            return session;
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private Status startVendorSessionLocked(VendorVibrationSession session) {
+        Trace.traceBegin(TRACE_TAG_VIBRATOR, "startSessionLocked");
+        try {
+            if (session.isEnded()) {
+                // Session already ended, possibly cancelled by app cancellation signal.
+                return session.getStatus();
+            }
+            if (!session.linkToDeath()) {
+                return Status.IGNORED_ERROR_TOKEN;
+            }
+            if (!mNativeWrapper.startSession(session.getSessionId(), session.getVibratorIds())) {
+                Slog.e(TAG, "Error starting session " + session.getSessionId()
+                        + " on vibrators " + Arrays.toString(session.getVibratorIds()));
+                session.unlinkToDeath();
+                return Status.IGNORED_UNSUPPORTED;
+            }
+            session.notifyStart();
+            mCurrentSession = session;
+            return null;
+        } finally {
+            Trace.traceEnd(TRACE_TAG_VIBRATOR);
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -747,8 +908,8 @@
 
             pw.println("CurrentVibration:");
             pw.increaseIndent();
-            if (mCurrentVibration != null) {
-                mCurrentVibration.getDebugInfo().dump(pw);
+            if (mCurrentSession != null) {
+                mCurrentSession.getDebugInfo().dump(pw);
             } else {
                 pw.println("null");
             }
@@ -757,8 +918,8 @@
 
             pw.println("NextVibration:");
             pw.increaseIndent();
-            if (mNextVibration != null) {
-                mNextVibration.getDebugInfo().dump(pw);
+            if (mNextSession != null) {
+                mNextSession.getDebugInfo().dump(pw);
             } else {
                 pw.println("null");
             }
@@ -782,8 +943,8 @@
         synchronized (mLock) {
             mVibrationSettings.dump(proto);
             mVibrationScaler.dump(proto);
-            if (mCurrentVibration != null) {
-                mCurrentVibration.getDebugInfo().dump(proto,
+            if (mCurrentSession != null) {
+                mCurrentSession.getDebugInfo().dump(proto,
                         VibratorManagerServiceDumpProto.CURRENT_VIBRATION);
             }
             for (int i = 0; i < mVibrators.size(); i++) {
@@ -816,18 +977,18 @@
             }
 
             // TODO(b/372241975): investigate why external vibrations were not handled here before
-            if (mCurrentVibration == null
-                    || (mCurrentVibration instanceof ExternalVibrationSession)) {
+            if (mCurrentSession == null
+                    || (mCurrentSession instanceof ExternalVibrationSession)) {
                 return;
             }
 
-            Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentVibration.getCallerInfo());
+            Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentSession.getCallerInfo());
             if (inputDevicesChanged || (ignoreStatus != null)) {
                 if (DEBUG) {
                     Slog.d(TAG, "Canceling vibration because settings changed: "
                             + (inputDevicesChanged ? "input devices changed" : ignoreStatus));
                 }
-                mCurrentVibration.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE);
+                mCurrentSession.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE);
             }
         }
     }
@@ -866,15 +1027,15 @@
             if (mInputDeviceDelegate.isAvailable()) {
                 return startVibrationOnInputDevicesLocked(session.getVibration());
             }
-            if (mCurrentVibration == null) {
+            if (mCurrentSession == null) {
                 return startVibrationOnThreadLocked(session);
             }
             // If there's already a vibration queued (waiting for the previous one to finish
             // cancelling), end it cleanly and replace it with the new one.
             // Note that we don't consider pipelining here, because new pipelined ones should
             // replace pending non-executing pipelined ones anyway.
-            clearNextVibrationLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo());
-            mNextVibration = session;
+            clearNextSessionLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo());
+            mNextSession = session;
             return null;
         } finally {
             Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -891,16 +1052,16 @@
             case AppOpsManager.MODE_ALLOWED:
                 Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
                 // Make sure mCurrentVibration is set while triggering the VibrationThread.
-                mCurrentVibration = session;
-                if (!mCurrentVibration.linkToDeath()) {
+                mCurrentSession = session;
+                if (!mCurrentSession.linkToDeath()) {
                     // Shouldn't happen. The method call already logs.
-                    mCurrentVibration = null;  // Aborted.
+                    mCurrentSession = null;  // Aborted.
                     return Status.IGNORED_ERROR_TOKEN;
                 }
                 if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) {
                     // Shouldn't happen. The method call already logs.
                     session.setVibrationConductor(null); // Rejected by thread, clear it in session.
-                    mCurrentVibration = null;  // Aborted.
+                    mCurrentSession = null;  // Aborted.
                     return Status.IGNORED_ERROR_SCHEDULING;
                 }
                 return null;
@@ -914,23 +1075,29 @@
     }
 
     @GuardedBy("mLock")
-    private void maybeStartNextSingleVibrationLocked() {
-        if (mNextVibration instanceof SingleVibrationSession session) {
-            mNextVibration = null;
+    private void maybeStartNextSessionLocked() {
+        if (mNextSession instanceof SingleVibrationSession session) {
+            mNextSession = null;
             Status errorStatus = startVibrationOnThreadLocked(session);
             if (errorStatus != null) {
-                endVibrationLocked(session, errorStatus);
+                endSessionLocked(session, errorStatus);
             }
-        }
+        } else if (mNextSession instanceof VendorVibrationSession session) {
+            mNextSession = null;
+            Status errorStatus = startVendorSessionLocked(session);
+            if (errorStatus != null) {
+                endSessionLocked(session, errorStatus);
+            }
+        } // External vibrations cannot be started asynchronously.
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(VibrationSession session, Status status) {
-        endVibrationLocked(session, status, /* endedBy= */ null);
+    private void endSessionLocked(VibrationSession session, Status status) {
+        endSessionLocked(session, status, /* endedBy= */ null);
     }
 
     @GuardedBy("mLock")
-    private void endVibrationLocked(VibrationSession session, Status status, CallerInfo endedBy) {
+    private void endSessionLocked(VibrationSession session, Status status, CallerInfo endedBy) {
         session.requestEnd(status, endedBy, /* immediate= */ false);
         logAndRecordVibration(session.getDebugInfo());
     }
@@ -975,6 +1142,13 @@
                         VibrationScaler.ADAPTIVE_SCALE_NONE));
     }
 
+    private void logAndRecordSessionAttempt(CallerInfo callerInfo, Status status) {
+        logAndRecordVibration(
+                new VendorVibrationSession.DebugInfoImpl(status, callerInfo,
+                        SystemClock.uptimeMillis(), System.currentTimeMillis(),
+                        /* startTime= */ 0, /* endUptime= */ 0, /* endTime= */ 0));
+    }
+
     private void logAndRecordVibration(DebugInfo info) {
         info.logMetrics(mFrameworkStatsLogger);
         logVibrationStatus(info.getCallerInfo().uid, info.getCallerInfo().attrs, info.getStatus());
@@ -1026,25 +1200,40 @@
         }
     }
 
+    private void onVibrationSessionComplete(long sessionId) {
+        synchronized (mLock) {
+            if (mCurrentSession == null || mCurrentSession.getSessionId() != sessionId) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Vibration session " + sessionId + " callback ignored");
+                }
+                return;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId + " complete, notifying session");
+            }
+            mCurrentSession.notifySessionCallback();
+        }
+    }
+
     private void onSyncedVibrationComplete(long vibrationId) {
         synchronized (mLock) {
-            if (mCurrentVibration != null) {
+            if (mCurrentSession != null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread");
                 }
-                mCurrentVibration.notifySyncedVibratorsCallback(vibrationId);
+                mCurrentSession.notifySyncedVibratorsCallback(vibrationId);
             }
         }
     }
 
     private void onVibrationComplete(int vibratorId, long vibrationId) {
         synchronized (mLock) {
-            if (mCurrentVibration != null) {
+            if (mCurrentSession != null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId
                             + " complete, notifying thread");
                 }
-                mCurrentVibration.notifyVibratorCallback(vibratorId, vibrationId);
+                mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId);
             }
         }
     }
@@ -1056,10 +1245,10 @@
      */
     @GuardedBy("mLock")
     @Nullable
-    private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(VibrationSession session) {
-        if (mNextVibration != null) {
-            Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(session,
-                    mNextVibration);
+    private Vibration.EndInfo shouldIgnoreForOngoingLocked(VibrationSession session) {
+        if (mNextSession != null) {
+            Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoing(session,
+                    mNextSession);
             if (vibrationEndInfo != null) {
                 // Next vibration has higher importance than the new one, so the new vibration
                 // should be ignored.
@@ -1067,13 +1256,13 @@
             }
         }
 
-        if (mCurrentVibration != null) {
-            if (mCurrentVibration.wasEndRequested()) {
+        if (mCurrentSession != null) {
+            if (mCurrentSession.wasEndRequested()) {
                 // Current session has ended or is cancelling, should not block incoming vibrations.
                 return null;
             }
 
-            return shouldIgnoreVibrationForOngoing(session, mCurrentVibration);
+            return shouldIgnoreForOngoing(session, mCurrentSession);
         }
 
         return null;
@@ -1086,7 +1275,7 @@
      * @return a Vibration.EndInfo if the vibration should be ignored, null otherwise.
      */
     @Nullable
-    private static Vibration.EndInfo shouldIgnoreVibrationForOngoing(
+    private static Vibration.EndInfo shouldIgnoreForOngoing(
             @NonNull VibrationSession newSession, @NonNull VibrationSession ongoingSession) {
 
         int newSessionImportance = getVibrationImportance(newSession);
@@ -1214,11 +1403,15 @@
      * @param tokenFilter The binder token to identify the vibration origin. Only vibrations
      *                    started with the same token can be cancelled with it.
      */
-    private boolean shouldCancelVibration(@Nullable VibrationSession session, int usageFilter,
+    private boolean shouldCancelSession(@Nullable VibrationSession session, int usageFilter,
             @Nullable IBinder tokenFilter) {
         if (session == null) {
             return false;
         }
+        if (session instanceof VendorVibrationSession) {
+            // Vendor sessions should not be cancelled by Vibrator.cancel API.
+            return false;
+        }
         if ((tokenFilter != null) && (tokenFilter != session.getCallerToken())) {
             // Vibration from a different app, this should not cancel it.
             return false;
@@ -1572,10 +1765,10 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
             try {
                 synchronized (mLock) {
-                    if (!(mCurrentVibration instanceof SingleVibrationSession session)) {
+                    if (!(mCurrentSession instanceof SingleVibrationSession session)) {
                         if (Build.IS_DEBUGGABLE) {
                             Slog.wtf(TAG, "VibrationSession invalid on vibration thread release."
-                                    + " currentSession=" + mCurrentVibration);
+                                    + " currentSession=" + mCurrentSession);
                         }
                         // Only single vibration sessions are ended by thread being released. Abort.
                         return;
@@ -1586,11 +1779,11 @@
                                         + " expected=%d, released=%d",
                                 session.getVibration().id, vibrationId));
                     }
-                    finishAppOpModeLocked(mCurrentVibration.getCallerInfo());
-                    clearCurrentVibrationLocked();
+                    finishAppOpModeLocked(mCurrentSession.getCallerInfo());
+                    clearCurrentSessionLocked();
                     Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
-                    // Start next vibration if it's a single vibration waiting for the thread.
-                    maybeStartNextSingleVibrationLocked();
+                    // Start next vibration if it's waiting for the thread.
+                    maybeStartNextSessionLocked();
                 }
             } finally {
                 Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -1613,10 +1806,10 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationReleased");
             try {
                 synchronized (mLock) {
-                    if (!(mCurrentVibration instanceof ExternalVibrationSession session)) {
+                    if (!(mCurrentSession instanceof ExternalVibrationSession session)) {
                         if (Build.IS_DEBUGGABLE) {
                             Slog.wtf(TAG, "VibrationSession invalid on external vibration release."
-                                    + " currentSession=" + mCurrentVibration);
+                                    + " currentSession=" + mCurrentSession);
                         }
                         // Only external vibration sessions are ended by this callback. Abort.
                         return;
@@ -1627,10 +1820,9 @@
                                         + " expected=%d, released=%d", session.id, vibrationId));
                     }
                     setExternalControl(false, session.stats);
-                    clearCurrentVibrationLocked();
-                    // Start next vibration if it's a single vibration waiting for the external
-                    // control to be over.
-                    maybeStartNextSingleVibrationLocked();
+                    clearCurrentSessionLocked();
+                    // Start next vibration if it's waiting for the external control to be over.
+                    maybeStartNextSessionLocked();
                 }
             } finally {
                 Trace.traceEnd(TRACE_TAG_VIBRATOR);
@@ -1638,19 +1830,75 @@
         }
     }
 
-    /** Listener for synced vibration completion callbacks from native. */
+    /**
+     * Implementation of {@link ExternalVibrationSession.VibratorManagerHooks} that controls
+     * external vibrations and reports them when finished.
+     */
+    private final class VendorVibrationSessionCallbacks
+            implements VendorVibrationSession.VibratorManagerHooks {
+
+        @Override
+        public void endSession(long sessionId, boolean shouldAbort) {
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId
+                        + (shouldAbort ? " aborting" : " ending"));
+            }
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "endSession");
+            try {
+                mNativeWrapper.endSession(sessionId, shouldAbort);
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
+            }
+        }
+
+        @Override
+        public void onSessionReleased(long sessionId) {
+            if (DEBUG) {
+                Slog.d(TAG, "Vibration session " + sessionId + " released");
+            }
+            Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVendorSessionReleased");
+            try {
+                synchronized (mLock) {
+                    if (!(mCurrentSession instanceof VendorVibrationSession session)) {
+                        if (Build.IS_DEBUGGABLE) {
+                            Slog.wtf(TAG, "VibrationSession invalid on vibration session release."
+                                    + " currentSession=" + mCurrentSession);
+                        }
+                        // Only vendor vibration sessions are ended by this callback. Abort.
+                        return;
+                    }
+                    if (Build.IS_DEBUGGABLE && (session.getSessionId() != sessionId)) {
+                        Slog.wtf(TAG, TextUtils.formatSimple(
+                                "SessionId mismatch on vendor vibration session release."
+                                        + " expected=%d, released=%d",
+                                session.getSessionId(), sessionId));
+                    }
+                    clearCurrentSessionLocked();
+                    // Start next vibration if it's waiting for the HAL session to be over.
+                    maybeStartNextSessionLocked();
+                }
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIBRATOR);
+            }
+        }
+    }
+
+    /** Listener for vibrator manager completion callbacks from native. */
     @VisibleForTesting
-    interface OnSyncedVibrationCompleteListener {
+    interface VibratorManagerNativeCallbacks {
 
         /** Callback triggered when synced vibration is complete. */
-        void onComplete(long vibrationId);
+        void onSyncedVibrationComplete(long vibrationId);
+
+        /** Callback triggered when vibration session is complete. */
+        void onVibrationSessionComplete(long sessionId);
     }
 
     /**
      * Implementation of listeners to native vibrators with a weak reference to this service.
      */
     private static final class VibrationCompleteListener implements
-            VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener {
+            VibratorController.OnVibrationCompleteListener, VibratorManagerNativeCallbacks {
         private WeakReference<VibratorManagerService> mServiceRef;
 
         VibrationCompleteListener(VibratorManagerService service) {
@@ -1658,7 +1906,7 @@
         }
 
         @Override
-        public void onComplete(long vibrationId) {
+        public void onSyncedVibrationComplete(long vibrationId) {
             VibratorManagerService service = mServiceRef.get();
             if (service != null) {
                 service.onSyncedVibrationComplete(vibrationId);
@@ -1666,6 +1914,14 @@
         }
 
         @Override
+        public void onVibrationSessionComplete(long sessionId) {
+            VibratorManagerService service = mServiceRef.get();
+            if (service != null) {
+                service.onVibrationSessionComplete(sessionId);
+            }
+        }
+
+        @Override
         public void onComplete(int vibratorId, long vibrationId) {
             VibratorManagerService service = mServiceRef.get();
             if (service != null) {
@@ -1698,7 +1954,7 @@
         private long mNativeServicePtr = 0;
 
         /** Returns native pointer to newly created controller and connects with HAL service. */
-        public void init(OnSyncedVibrationCompleteListener listener) {
+        public void init(VibratorManagerNativeCallbacks listener) {
             mNativeServicePtr = nativeInit(listener);
             long finalizerPtr = nativeGetFinalizer();
 
@@ -1734,6 +1990,21 @@
         public void cancelSynced() {
             nativeCancelSynced(mNativeServicePtr);
         }
+
+        /** Start vibration session. */
+        public boolean startSession(long sessionId, @NonNull int[] vibratorIds) {
+            return nativeStartSession(mNativeServicePtr, sessionId, vibratorIds);
+        }
+
+        /** End vibration session. */
+        public void endSession(long sessionId, boolean shouldAbort) {
+            nativeEndSession(mNativeServicePtr, sessionId, shouldAbort);
+        }
+
+        /** Clear vibration sessions. */
+        public void clearSessions() {
+            nativeClearSessions(mNativeServicePtr);
+        }
     }
 
     /** Keep records of vibrations played and provide debug information for this service. */
@@ -1853,46 +2124,46 @@
 
     /** Clears mNextVibration if set, ending it cleanly */
     @GuardedBy("mLock")
-    private void clearNextVibrationLocked(Status status) {
-        clearNextVibrationLocked(status, /* endedBy= */ null);
+    private void clearNextSessionLocked(Status status) {
+        clearNextSessionLocked(status, /* endedBy= */ null);
     }
 
     /** Clears mNextVibration if set, ending it cleanly */
     @GuardedBy("mLock")
-    private void clearNextVibrationLocked(Status status, CallerInfo endedBy) {
-        if (mNextVibration != null) {
+    private void clearNextSessionLocked(Status status, CallerInfo endedBy) {
+        if (mNextSession != null) {
             if (DEBUG) {
-                Slog.d(TAG, "Dropping pending vibration from " + mNextVibration.getCallerInfo()
+                Slog.d(TAG, "Dropping pending vibration from " + mNextSession.getCallerInfo()
                         + " with status: " + status);
             }
             // Clearing next vibration before playing it, end it and report metrics right away.
-            endVibrationLocked(mNextVibration, status, endedBy);
-            mNextVibration = null;
+            endSessionLocked(mNextSession, status, endedBy);
+            mNextSession = null;
         }
     }
 
     /** Clears mCurrentVibration if set, reporting metrics */
     @GuardedBy("mLock")
-    private void clearCurrentVibrationLocked() {
-        if (mCurrentVibration != null) {
-            mCurrentVibration.unlinkToDeath();
-            logAndRecordVibration(mCurrentVibration.getDebugInfo());
-            mCurrentVibration = null;
+    private void clearCurrentSessionLocked() {
+        if (mCurrentSession != null) {
+            mCurrentSession.unlinkToDeath();
+            logAndRecordVibration(mCurrentSession.getDebugInfo());
+            mCurrentSession = null;
             mLock.notify(); // Notify if waiting for current vibration to end.
         }
     }
 
     @GuardedBy("mLock")
-    private void maybeClearCurrentAndNextVibrationsLocked(
+    private void maybeClearCurrentAndNextSessionsLocked(
             Predicate<VibrationSession> shouldEndSessionPredicate, Status endStatus) {
         // TODO(b/372241975): investigate why external vibrations were not handled here before
-        if (!(mNextVibration instanceof ExternalVibrationSession)
-                && shouldEndSessionPredicate.test(mNextVibration)) {
-            clearNextVibrationLocked(endStatus);
+        if (!(mNextSession instanceof ExternalVibrationSession)
+                && shouldEndSessionPredicate.test(mNextSession)) {
+            clearNextSessionLocked(endStatus);
         }
-        if (!(mCurrentVibration instanceof ExternalVibrationSession)
-                && shouldEndSessionPredicate.test(mCurrentVibration)) {
-            mCurrentVibration.requestEnd(endStatus);
+        if (!(mCurrentSession instanceof ExternalVibrationSession)
+                && shouldEndSessionPredicate.test(mCurrentSession)) {
+            mCurrentSession.requestEnd(endStatus);
         }
     }
 
@@ -1902,12 +2173,12 @@
      *
      * @return true if the vibration completed, or false if waiting timed out.
      */
-    public boolean waitForCurrentVibrationEnd(long maxWaitMillis) {
+    public boolean waitForCurrentSessionEnd(long maxWaitMillis) {
         long now = SystemClock.elapsedRealtime();
         long deadline = now + maxWaitMillis;
         synchronized (mLock) {
             while (true) {
-                if (mCurrentVibration == null) {
+                if (mCurrentSession == null) {
                     return true;  // Done
                 }
                 if (now >= deadline) {  // Note that thread.wait(0) waits indefinitely.
@@ -1965,7 +2236,7 @@
 
                 synchronized (mLock) {
                     if (!hasExternalControlCapability()) {
-                        endVibrationLocked(session, Status.IGNORED_UNSUPPORTED);
+                        endSessionLocked(session, Status.IGNORED_UNSUPPORTED);
                         return session.getScale();
                     }
 
@@ -1976,17 +2247,17 @@
                         Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
                                 + " tried to play externally controlled vibration"
                                 + " without VIBRATE permission, ignoring.");
-                        endVibrationLocked(session, Status.IGNORED_MISSING_PERMISSION);
+                        endSessionLocked(session, Status.IGNORED_MISSING_PERMISSION);
                         return session.getScale();
                     }
 
                     Status ignoreStatus = shouldIgnoreVibrationLocked(session.callerInfo);
                     if (ignoreStatus != null) {
-                        endVibrationLocked(session, ignoreStatus);
+                        endSessionLocked(session, ignoreStatus);
                         return session.getScale();
                     }
 
-                    if ((mCurrentVibration instanceof ExternalVibrationSession evs)
+                    if ((mCurrentSession instanceof ExternalVibrationSession evs)
                             && evs.isHoldingSameVibration(vib)) {
                         // We are already playing this external vibration, so we can return the same
                         // scale calculated in the previous call to this method.
@@ -1994,17 +2265,17 @@
                     }
 
                     // Check if ongoing vibration is more important than this vibration.
-                    Vibration.EndInfo ignoreInfo = shouldIgnoreVibrationForOngoingLocked(session);
+                    Vibration.EndInfo ignoreInfo = shouldIgnoreForOngoingLocked(session);
                     if (ignoreInfo != null) {
-                        endVibrationLocked(session, ignoreInfo.status, ignoreInfo.endedBy);
+                        endSessionLocked(session, ignoreInfo.status, ignoreInfo.endedBy);
                         return session.getScale();
                     }
 
                     // First clear next request, so it won't start when the current one ends.
-                    clearNextVibrationLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo);
-                    mNextVibration = session;
+                    clearNextSessionLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo);
+                    mNextSession = session;
 
-                    if (mCurrentVibration != null) {
+                    if (mCurrentSession != null) {
                         // Cancel any vibration that may be playing and ready the vibrator, even if
                         // we have an externally controlled vibration playing already.
                         // Since the interface defines that only one externally controlled
@@ -2016,36 +2287,36 @@
                         // as we would need to mute the old one still if it came from a different
                         // controller.
                         session.stats.reportInterruptedAnotherVibration(
-                                mCurrentVibration.getCallerInfo());
-                        mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED,
+                                mCurrentSession.getCallerInfo());
+                        mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED,
                                 session.callerInfo, /* immediate= */ true);
                         waitForCompletion = true;
                     }
                 }
                 // Wait for lock and interact with HAL to set external control outside main lock.
                 if (waitForCompletion) {
-                    if (!waitForCurrentVibrationEnd(VIBRATION_CANCEL_WAIT_MILLIS)) {
+                    if (!waitForCurrentSessionEnd(VIBRATION_CANCEL_WAIT_MILLIS)) {
                         Slog.e(TAG, "Timed out waiting for vibration to cancel");
                         synchronized (mLock) {
-                            if (mNextVibration == session) {
-                                mNextVibration = null;
+                            if (mNextSession == session) {
+                                mNextSession = null;
                             }
-                            endVibrationLocked(session, Status.IGNORED_ERROR_CANCELLING);
+                            endSessionLocked(session, Status.IGNORED_ERROR_CANCELLING);
                             return session.getScale();
                         }
                     }
                 }
                 synchronized (mLock) {
-                    if (mNextVibration == session) {
+                    if (mNextSession == session) {
                         // This is still the next vibration to be played.
-                        mNextVibration = null;
+                        mNextSession = null;
                     } else {
                         // A new request took the place of this one, maybe with higher importance.
                         // Next vibration already cleared with the right status, just return here.
                         return session.getScale();
                     }
                     if (!session.linkToDeath()) {
-                        endVibrationLocked(session, Status.IGNORED_ERROR_TOKEN);
+                        endSessionLocked(session, Status.IGNORED_ERROR_TOKEN);
                         return session.getScale();
                     }
                     if (DEBUG) {
@@ -2062,7 +2333,7 @@
                         // should be ignored or scaled.
                         mVibrationSettings.update();
                     }
-                    mCurrentVibration = session;
+                    mCurrentSession = session;
                     session.scale(mVibrationScaler, attrs.getUsage());
 
                     // Vibrator will start receiving data from external channels after this point.
@@ -2080,12 +2351,12 @@
             Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
             try {
                 synchronized (mLock) {
-                    if ((mCurrentVibration instanceof ExternalVibrationSession evs)
+                    if ((mCurrentSession instanceof ExternalVibrationSession evs)
                             && evs.isHoldingSameVibration(vib)) {
                         if (DEBUG) {
                             Slog.d(TAG, "Stopping external vibration: " + vib);
                         }
-                        mCurrentVibration.requestEnd(Status.FINISHED);
+                        mCurrentSession.requestEnd(Status.FINISHED);
                     }
                 }
             } finally {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5cff37a..10f096c 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -858,10 +858,12 @@
                             wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo,
                             wallpaper.getDescription());
                 } else {
+                    WallpaperDescription desc = new WallpaperDescription.Builder().setComponent(
+                            (connection.mInfo != null) ? connection.mInfo.getComponent()
+                                    : null).build();
                     connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false,
                             wpdData.mWidth, wpdData.mHeight,
-                            wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo,
-                            /* description= */ null);
+                            wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, desc);
                 }
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed attaching wallpaper on display", e);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 73ae51c..14be59f 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -254,6 +254,7 @@
 import static org.xmlpull.v1.XmlPullParser.END_TAG;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
+import android.Manifest;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -10308,6 +10309,21 @@
         if (pictureInPictureArgs != null && pictureInPictureArgs.hasSourceBoundsHint()) {
             pictureInPictureArgs.getSourceRectHint().offset(windowBounds.left, windowBounds.top);
         }
+
+        if (android.app.Flags.enableTvImplicitEnterPipRestriction()) {
+            PackageManager pm = mAtmService.mContext.getPackageManager();
+            if (pictureInPictureArgs.isAutoEnterEnabled()
+                    && pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+                    && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP, packageName)
+                    == PackageManager.PERMISSION_DENIED) {
+                Log.i(TAG,
+                        "Auto-enter PiP only allowed on TV if android.permission"
+                                + ".TV_IMPLICIT_ENTER_PIP permission is held by the app.");
+                PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
+                builder.setAutoEnterEnabled(false);
+                pictureInPictureArgs.copyOnlySet(builder.build());
+            }
+        }
     }
 
     private void applyLocaleOverrideIfNeeded(Configuration resolvedConfig) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index ec171c5..a71620d 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -46,10 +46,8 @@
 import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
 import static com.android.window.flags.Flags.balAdditionalStartModes;
 import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg;
-import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck;
 import static com.android.window.flags.Flags.balImprovedMetrics;
 import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
-import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid;
 import static com.android.window.flags.Flags.balShowToastsBlocked;
 import static com.android.window.flags.Flags.balStrictModeRo;
 
@@ -348,11 +346,7 @@
             @BackgroundActivityStartMode int realCallerBackgroundActivityStartMode =
                     checkedOptions.getPendingIntentBackgroundActivityStartMode();
 
-            if (!balImproveRealCallerVisibilityCheck()) {
-                // without this fix the auto-opt ins below would violate CTS tests
-                mAutoOptInReason = null;
-                mAutoOptInCaller = false;
-            } else if (originatingPendingIntent == null) {
+            if (originatingPendingIntent == null) {
                 mAutoOptInReason = AUTO_OPT_IN_NOT_PENDING_INTENT;
                 mAutoOptInCaller = true;
             } else if (mIsCallForResult) {
@@ -599,12 +593,8 @@
                         mCheckedOptions.getPendingIntentBackgroundActivityStartMode()));
             }
             // features
-            sb.append("; balImproveRealCallerVisibilityCheck: ")
-                    .append(balImproveRealCallerVisibilityCheck());
             sb.append("; balRequireOptInByPendingIntentCreator: ")
                     .append(balRequireOptInByPendingIntentCreator());
-            sb.append("; balRespectAppSwitchStateWhenCheckBoundByForegroundUid: ")
-                    .append(balRespectAppSwitchStateWhenCheckBoundByForegroundUid());
             sb.append("; balDontBringExistingBackgroundTaskStackToFg: ")
                     .append(balDontBringExistingBackgroundTaskStackToFg());
             sb.append("]");
@@ -1133,23 +1123,13 @@
         final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
                 || state.mAppSwitchState == APP_SWITCH_FG_ONLY
                 || isHomeApp(state.mRealCallingUid, state.mRealCallingPackage);
-        if (balImproveRealCallerVisibilityCheck()) {
-            if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) {
-                return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
-                        /*background*/ false, "realCallingUid has visible window");
-            }
-            if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) {
-                return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
-                        /*background*/ false, "realCallingUid has non-app visible window");
-            }
-        } else {
-            // don't abort if the realCallingUid has a visible window
-            // TODO(b/171459802): We should check appSwitchAllowed also
-            if (state.mRealCallingUidHasAnyVisibleWindow) {
-                return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
-                        /*background*/ false,
-                        "realCallingUid has visible (non-toast) window.");
-            }
+        if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) {
+            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+                    /*background*/ false, "realCallingUid has visible window");
+        }
+        if (mService.mActiveUids.hasNonAppVisibleWindow(state.mRealCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
+                    /*background*/ false, "realCallingUid has non-app visible window");
         }
 
         // Don't abort if the realCallerApp or other processes of that uid are considered to be in
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index 264c8be..ccf1aed 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -50,7 +50,6 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.wm.BackgroundActivityStartController.BalVerdict;
-import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -137,10 +136,8 @@
         }
         // Allow if the caller is bound by a UID that's currently foreground.
         // But still respect the appSwitchState.
-        if (checkConfiguration.checkVisibility && (
-                Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid()
-                        ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid()
-                        : isBoundByForegroundUid())) {
+        if (checkConfiguration.checkVisibility && appSwitchState != APP_SWITCH_DISALLOW
+                && isBoundByForegroundUid()) {
             return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_BOUND_BY_FOREGROUND
                     : BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false,
                     "process bound by foreground uid");
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 3f6e915..9a48d5b 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -18,7 +18,6 @@
 
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;
 import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_ADAPTER;
-import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_START_DELAYED;
 import static com.android.server.wm.SurfaceAnimatorProto.LEASH;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -90,8 +89,6 @@
     @Nullable
     private Runnable mAnimationCancelledCallback;
 
-    private boolean mAnimationStartDelayed;
-
     private boolean mAnimationFinished;
 
     /**
@@ -188,10 +185,6 @@
             mAnimatable.onAnimationLeashCreated(t, mLeash);
         }
         mAnimatable.onLeashAnimationStarting(t, mLeash);
-        if (mAnimationStartDelayed) {
-            ProtoLog.i(WM_DEBUG_ANIM, "Animation start delayed for %s", mAnimatable);
-            return;
-        }
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
         if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) {
             StringWriter sw = new StringWriter();
@@ -215,36 +208,7 @@
                 null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */);
     }
 
-    /**
-     * Begins with delaying all animations to start. Any subsequent call to {@link #startAnimation}
-     * will not start the animation until {@link #endDelayingAnimationStart} is called. When an
-     * animation start is being delayed, the animator is considered animating already.
-     */
-    void startDelayingAnimationStart() {
-
-        // We only allow delaying animation start we are not currently animating
-        if (!isAnimating()) {
-            mAnimationStartDelayed = true;
-        }
-    }
-
-    /**
-     * See {@link #startDelayingAnimationStart}.
-     */
-    void endDelayingAnimationStart() {
-        final boolean delayed = mAnimationStartDelayed;
-        mAnimationStartDelayed = false;
-        if (delayed && mAnimation != null) {
-            mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(),
-                    mAnimationType, mInnerAnimationFinishedCallback);
-            mAnimatable.commitPendingTransaction();
-        }
-    }
-
-    /**
-     * @return Whether we are currently running an animation, or we have a pending animation that
-     *         is waiting to be started with {@link #endDelayingAnimationStart}
-     */
+    /** Returns whether it is currently running an animation. */
     boolean isAnimating() {
         return mAnimation != null;
     }
@@ -290,15 +254,6 @@
     }
 
     /**
-     * Reparents the surface.
-     *
-     * @see #setLayer
-     */
-    void reparent(Transaction t, SurfaceControl newParent) {
-        t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent);
-    }
-
-    /**
      * @return True if the surface is attached to the leash; false otherwise.
      */
     boolean hasLeash() {
@@ -319,7 +274,6 @@
             Slog.w(TAG, "Unable to transfer animation, because " + from + " animation is finished");
             return;
         }
-        endDelayingAnimationStart();
         final Transaction t = mAnimatable.getSyncTransaction();
         cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
         mLeash = from.mLeash;
@@ -336,10 +290,6 @@
         mService.mAnimationTransferMap.put(mAnimation, this);
     }
 
-    boolean isAnimationStartDelayed() {
-        return mAnimationStartDelayed;
-    }
-
     /**
      * Cancels the animation, and resets the leash.
      *
@@ -361,7 +311,7 @@
         final SurfaceFreezer.Snapshot snapshot = mSnapshot;
         reset(t, false);
         if (animation != null) {
-            if (!mAnimationStartDelayed && forwardCancel) {
+            if (forwardCancel) {
                 animation.onAnimationCancelled(leash);
                 if (animationCancelledCallback != null) {
                     animationCancelledCallback.run();
@@ -386,10 +336,6 @@
                 mService.scheduleAnimationLocked();
             }
         }
-
-        if (!restarting) {
-            mAnimationStartDelayed = false;
-        }
     }
 
     private void reset(Transaction t, boolean destroyLeash) {
@@ -495,14 +441,12 @@
         if (mLeash != null) {
             mLeash.dumpDebug(proto, LEASH);
         }
-        proto.write(ANIMATION_START_DELAYED, mAnimationStartDelayed);
         proto.end(token);
     }
 
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("mLeash="); pw.print(mLeash);
-        pw.print(" mAnimationType=" + animationTypeToString(mAnimationType));
-        pw.println(mAnimationStartDelayed ? " mAnimationStartDelayed=true" : "");
+        pw.print(" mAnimationType="); pw.println(animationTypeToString(mAnimationType));
         pw.print(prefix); pw.print("Animation: "); pw.println(mAnimation);
         if (mAnimation != null) {
             mAnimation.dump(pw, prefix + "  ");
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a603466..20481f2 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -465,6 +465,31 @@
         return false;
     }
 
+    /**
+     * This ensures that all changes for previously transient-hide containers are flagged such that
+     * they will report changes and be included in this transition.
+     */
+    void updateChangesForRestoreTransientHideTasks(Transition transientLaunchTransition) {
+        if (transientLaunchTransition.mTransientHideTasks == null) {
+            // Skip if the transient-launch transition has no transient-hide tasks
+            ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                    "Skipping update changes for restore transient hide tasks");
+            return;
+        }
+
+        // For each change, if it was previously transient-hidden, then we should force a flag to
+        // ensure that it is included in the next transition
+        for (int i = 0; i < mChanges.size(); i++) {
+            final WindowContainer container = mChanges.keyAt(i);
+            if (transientLaunchTransition.isInTransientHide(container)) {
+                ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                        "Force update transient hide task for restore %d: %s", mSyncId, container);
+                final ChangeInfo info = mChanges.valueAt(i);
+                info.mRestoringTransientHide = true;
+            }
+        }
+    }
+
     /** Returns {@code true} if the task should keep visible if this is a transient transition. */
     boolean isTransientVisible(@NonNull Task task) {
         if (mTransientLaunches == null) return false;
@@ -3478,6 +3503,10 @@
 
         // State tracking
         boolean mExistenceChanged = false;
+        // This state indicates that we are restoring transient order as a part of an
+        // end-transition. Because the visibility for transient hide containers has not actually
+        // changed, we need to ensure that hasChanged() still reports the relevant changes
+        boolean mRestoringTransientHide = false;
         // before change state
         boolean mVisible;
         int mWindowingMode;
@@ -3552,7 +3581,11 @@
                     || !mContainer.getBounds().equals(mAbsoluteBounds)
                     || mRotation != mContainer.getWindowConfiguration().getRotation()
                     || mDisplayId != getDisplayId(mContainer)
-                    || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0;
+                    || (mFlags & ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP) != 0
+                    // If we are restoring transient-hide containers, then we should consider them
+                    // important for the transition as well (their requested visibilities would not
+                    // have changed for the checks below to consider it).
+                    || mRestoringTransientHide;
         }
 
         @TransitionInfo.TransitionMode
@@ -3565,6 +3598,11 @@
             }
             final boolean nowVisible = wc.isVisibleRequested();
             if (nowVisible == mVisible) {
+                if (mRestoringTransientHide) {
+                    // The requested visibility has not changed for transient-hide containers, but
+                    // we are restoring them so we should considering them moving to front again
+                    return TRANSIT_TO_FRONT;
+                }
                 return TRANSIT_CHANGE;
             }
             if (mExistenceChanged) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 87bdfa4..143d1b7 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -37,6 +37,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.TimeUtils;
@@ -524,6 +525,23 @@
         return false;
     }
 
+    /**
+     * @return A pair of the transition and restore-behind target for the given {@param container}.
+     * @param container An ancestor of a transient-launch activity
+     */
+    @Nullable
+    Pair<Transition, Task> getTransientLaunchTransitionAndTarget(
+            @NonNull WindowContainer container) {
+        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+            final Transition transition = mPlayingTransitions.get(i);
+            final Task restoreBehindTask = transition.getTransientLaunchRestoreTarget(container);
+            if (restoreBehindTask != null) {
+                return new Pair<>(transition, restoreBehindTask);
+            }
+        }
+        return null;
+    }
+
     /** Returns {@code true} if the display contains a transient-launch transition. */
     boolean hasTransientLaunch(@NonNull DisplayContent dc) {
         if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch()
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index e0c473d..5f92bb6 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3215,8 +3215,7 @@
         final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter
                 && isChangingAppTransition();
 
-        // Delaying animation start isn't compatible with remote animations at all.
-        if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) {
+        if (controller != null) {
             // Here we load App XML in order to read com.android.R.styleable#Animation_showBackdrop.
             boolean showBackdrop = false;
             // Optionally set backdrop color if App explicitly provides it through
@@ -3639,20 +3638,6 @@
         return getAnimatingContainer(PARENTS, ANIMATION_TYPE_ALL);
     }
 
-    /**
-     * @see SurfaceAnimator#startDelayingAnimationStart
-     */
-    void startDelayingAnimationStart() {
-        mSurfaceAnimator.startDelayingAnimationStart();
-    }
-
-    /**
-     * @see SurfaceAnimator#endDelayingAnimationStart
-     */
-    void endDelayingAnimationStart() {
-        mSurfaceAnimator.endDelayingAnimationStart();
-    }
-
     @Override
     public int getSurfaceWidth() {
         return mSurfaceControl.getWidth();
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index dac8f69..ead1282 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -111,6 +111,7 @@
 import android.util.AndroidRuntimeException;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Slog;
 import android.view.RemoteAnimationAdapter;
 import android.view.SurfaceControl;
@@ -1375,16 +1376,56 @@
                 break;
             }
             case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: {
-                if (!chain.isFinishing()) break;
+                if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                    // Only allow restoring transient order when finishing a transition
+                    if (!chain.isFinishing()) break;
+                }
+                // Validate the container
                 final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
-                if (container == null) break;
+                if (container == null) {
+                    ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                            "Restoring transient order: invalid container");
+                    break;
+                }
                 final Task thisTask = container.asActivityRecord() != null
                         ? container.asActivityRecord().getTask() : container.asTask();
-                if (thisTask == null) break;
-                final Task restoreAt = chain.mTransition.getTransientLaunchRestoreTarget(container);
-                if (restoreAt == null) break;
+                if (thisTask == null) {
+                    ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                            "Restoring transient order: invalid task");
+                    break;
+                }
+
+                // Find the task to restore behind
+                final Pair<Transition, Task> transientRestore =
+                        mTransitionController.getTransientLaunchTransitionAndTarget(container);
+                if (transientRestore == null) {
+                    ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                            "Restoring transient order: no restore task");
+                    break;
+                }
+                final Transition transientLaunchTransition = transientRestore.first;
+                final Task restoreAt = transientRestore.second;
+                ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+                        "Restoring transient order: restoring behind task=%d", restoreAt.mTaskId);
+
+                // Restore the position of the given container behind the target task
                 final TaskDisplayArea taskDisplayArea = thisTask.getTaskDisplayArea();
                 taskDisplayArea.moveRootTaskBehindRootTask(thisTask.getRootTask(), restoreAt);
+
+                if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                    // Because we are in a transient launch transition, the requested visibility of
+                    // tasks does not actually change for the transient-hide tasks, but we do want
+                    // the restoration of these transient-hide tasks to top to be a part of this
+                    // finish transition
+                    final Transition collectingTransition =
+                            mTransitionController.getCollectingTransition();
+                    if (collectingTransition != null) {
+                        collectingTransition.updateChangesForRestoreTransientHideTasks(
+                                transientLaunchTransition);
+                    }
+                }
+
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 break;
             }
             case HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER: {
diff --git a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
index a47ab9d..46be79e 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp
@@ -16,27 +16,32 @@
 
 #define LOG_TAG "VibratorManagerService"
 
+#include "com_android_server_vibrator_VibratorManagerService.h"
+
 #include <nativehelper/JNIHelp.h>
+#include <utils/Log.h>
+#include <utils/misc.h>
+#include <vibratorservice/VibratorManagerHalController.h>
+
+#include <unordered_map>
+
 #include "android_runtime/AndroidRuntime.h"
 #include "core_jni_helpers.h"
 #include "jni.h"
 
-#include <utils/Log.h>
-#include <utils/misc.h>
-
-#include <vibratorservice/VibratorManagerHalController.h>
-
-#include "com_android_server_vibrator_VibratorManagerService.h"
-
 namespace android {
 
 static JavaVM* sJvm = nullptr;
-static jmethodID sMethodIdOnComplete;
+static jmethodID sMethodIdOnSyncedVibrationComplete;
+static jmethodID sMethodIdOnVibrationSessionComplete;
 static std::mutex gManagerMutex;
 static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr;
 
 class NativeVibratorManagerService {
 public:
+    using IVibrationSession = aidl::android::hardware::vibrator::IVibrationSession;
+    using VibrationSessionConfig = aidl::android::hardware::vibrator::VibrationSessionConfig;
+
     NativeVibratorManagerService(JNIEnv* env, jobject callbackListener)
           : mHal(std::make_unique<vibrator::ManagerHalController>()),
             mCallbackListener(env->NewGlobalRef(callbackListener)) {
@@ -52,15 +57,69 @@
 
     vibrator::ManagerHalController* hal() const { return mHal.get(); }
 
-    std::function<void()> createCallback(jlong vibrationId) {
+    std::function<void()> createSyncedVibrationCallback(jlong vibrationId) {
         return [vibrationId, this]() {
             auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
-            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId);
+            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnSyncedVibrationComplete,
+                                   vibrationId);
         };
     }
 
+    std::function<void()> createVibrationSessionCallback(jlong sessionId) {
+        return [sessionId, this]() {
+            auto jniEnv = GetOrAttachJNIEnvironment(sJvm);
+            jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnVibrationSessionComplete,
+                                   sessionId);
+            std::lock_guard<std::mutex> lock(mSessionMutex);
+            auto it = mSessions.find(sessionId);
+            if (it != mSessions.end()) {
+                mSessions.erase(it);
+            }
+        };
+    }
+
+    bool startSession(jlong sessionId, const std::vector<int32_t>& vibratorIds) {
+        VibrationSessionConfig config;
+        auto callback = createVibrationSessionCallback(sessionId);
+        auto result = hal()->startSession(vibratorIds, config, callback);
+        if (!result.isOk()) {
+            return false;
+        }
+
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        mSessions[sessionId] = std::move(result.value());
+        return true;
+    }
+
+    void closeSession(jlong sessionId) {
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        auto it = mSessions.find(sessionId);
+        if (it != mSessions.end()) {
+            it->second->close();
+            // Keep session, it can still be aborted.
+        }
+    }
+
+    void abortSession(jlong sessionId) {
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        auto it = mSessions.find(sessionId);
+        if (it != mSessions.end()) {
+            it->second->abort();
+            mSessions.erase(it);
+        }
+    }
+
+    void clearSessions() {
+        hal()->clearSessions();
+        std::lock_guard<std::mutex> lock(mSessionMutex);
+        mSessions.clear();
+    }
+
 private:
+    std::mutex mSessionMutex;
     const std::unique_ptr<vibrator::ManagerHalController> mHal;
+    std::unordered_map<jlong, std::shared_ptr<IVibrationSession>> mSessions
+            GUARDED_BY(mSessionMutex);
     const jobject mCallbackListener;
 };
 
@@ -142,7 +201,7 @@
         ALOGE("nativeTriggerSynced failed because native service was not initialized");
         return JNI_FALSE;
     }
-    auto callback = service->createCallback(vibrationId);
+    auto callback = service->createSyncedVibrationCallback(vibrationId);
     return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE;
 }
 
@@ -156,8 +215,47 @@
     service->hal()->cancelSynced();
 }
 
+static jboolean nativeStartSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr,
+                                   jlong sessionId, jintArray vibratorIds) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeStartSession failed because native service was not initialized");
+        return JNI_FALSE;
+    }
+    jsize size = env->GetArrayLength(vibratorIds);
+    std::vector<int32_t> ids(size);
+    env->GetIntArrayRegion(vibratorIds, 0, size, reinterpret_cast<jint*>(ids.data()));
+    return service->startSession(sessionId, ids) ? JNI_TRUE : JNI_FALSE;
+}
+
+static void nativeEndSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, jlong sessionId,
+                             jboolean shouldAbort) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeEndSession failed because native service was not initialized");
+        return;
+    }
+    if (shouldAbort) {
+        service->abortSession(sessionId);
+    } else {
+        service->closeSession(sessionId);
+    }
+}
+
+static void nativeClearSessions(JNIEnv* env, jclass /* clazz */, jlong servicePtr) {
+    NativeVibratorManagerService* service =
+            reinterpret_cast<NativeVibratorManagerService*>(servicePtr);
+    if (service == nullptr) {
+        ALOGE("nativeClearSessions failed because native service was not initialized");
+        return;
+    }
+    service->clearSessions();
+}
+
 inline static constexpr auto sNativeInitMethodSignature =
-        "(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)J";
+        "(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)J";
 
 static const JNINativeMethod method_table[] = {
         {"nativeInit", sNativeInitMethodSignature, (void*)nativeInit},
@@ -167,15 +265,20 @@
         {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced},
         {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced},
         {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced},
+        {"nativeStartSession", "(JJ[I)Z", (void*)nativeStartSession},
+        {"nativeEndSession", "(JJZ)V", (void*)nativeEndSession},
+        {"nativeClearSessions", "(J)V", (void*)nativeClearSessions},
 };
 
 int register_android_server_vibrator_VibratorManagerService(JavaVM* jvm, JNIEnv* env) {
     sJvm = jvm;
     auto listenerClassName =
-            "com/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener";
+            "com/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks";
     jclass listenerClass = FindClassOrDie(env, listenerClassName);
-    sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V");
-
+    sMethodIdOnSyncedVibrationComplete =
+            GetMethodIDOrDie(env, listenerClass, "onSyncedVibrationComplete", "(J)V");
+    sMethodIdOnVibrationSessionComplete =
+            GetMethodIDOrDie(env, listenerClass, "onVibrationSessionComplete", "(J)V");
     return jniRegisterNativeMethods(env, "com/android/server/vibrator/VibratorManagerService",
                                     method_table, NELEM(method_table));
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3805c02..9759772 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -119,7 +119,6 @@
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.server.accessibility.AccessibilityManagerService;
 import com.android.server.accounts.AccountManagerService;
-import com.android.server.adaptiveauth.AdaptiveAuthService;
 import com.android.server.adb.AdbService;
 import com.android.server.alarm.AlarmManagerService;
 import com.android.server.am.ActivityManagerService;
@@ -205,6 +204,7 @@
 import com.android.server.os.DeviceIdentifiersPolicyService;
 import com.android.server.os.NativeTombstoneManagerService;
 import com.android.server.os.SchedulingPolicyService;
+import com.android.server.os.instrumentation.DynamicInstrumentationManagerService;
 import com.android.server.pdb.PersistentDataBlockService;
 import com.android.server.people.PeopleService;
 import com.android.server.permission.access.AccessCheckingService;
@@ -249,6 +249,7 @@
 import com.android.server.security.FileIntegrityService;
 import com.android.server.security.KeyAttestationApplicationIdProviderService;
 import com.android.server.security.KeyChainSystemService;
+import com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService;
 import com.android.server.security.advancedprotection.AdvancedProtectionService;
 import com.android.server.security.rkp.RemoteProvisioningService;
 import com.android.server.selinux.SelinuxAuditLogsService;
@@ -2650,8 +2651,8 @@
             t.traceEnd();
 
             if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
-                t.traceBegin("StartAdaptiveAuthService");
-                mSystemServiceManager.startService(AdaptiveAuthService.class);
+                t.traceBegin("StartAdaptiveAuthenticationService");
+                mSystemServiceManager.startService(AdaptiveAuthenticationService.class);
                 t.traceEnd();
             }
 
@@ -2890,6 +2891,13 @@
         mSystemServiceManager.startService(TracingServiceProxy.class);
         t.traceEnd();
 
+        // UprobeStats DynamicInstrumentationManager
+        if (com.android.art.flags.Flags.executableMethodFileOffsets()) {
+            t.traceBegin("StartDynamicInstrumentationManager");
+            mSystemServiceManager.startService(DynamicInstrumentationManagerService.class);
+            t.traceEnd();
+        }
+
         // It is now time to start up the app processes...
 
         t.traceBegin("MakeLockSettingsServiceReady");
diff --git a/services/proguard.flags b/services/proguard.flags
index cdd41ab..977bd19 100644
--- a/services/proguard.flags
+++ b/services/proguard.flags
@@ -82,7 +82,7 @@
 -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaJackDetector { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaMidiDevice { *; }
 -keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorController$OnVibrationCompleteListener { *; }
--keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$OnSyncedVibrationCompleteListener { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$VibratorManagerNativeCallbacks { *; }
 -keepclasseswithmembers,allowoptimization,allowaccessmodification class com.android.server.** {
   *** *FromNative(...);
 }
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp b/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp
new file mode 100644
index 0000000..2c2e5fd
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_system_performance",
+}
+
+android_test {
+    name: "DynamicInstrumentationManagerServiceTests",
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "hamcrest-library",
+        "platform-test-annotations",
+        "services.core",
+        "testables",
+        "truth",
+    ],
+
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: [
+        "device-tests",
+    ],
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml b/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml
new file mode 100644
index 0000000..4913d70
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.os.instrumentation" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.os.instrumentation"
+        android:label="DynamicInstrumentationmanagerService Unit Tests"/>
+</manifest>
\ No newline at end of file
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING b/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING
new file mode 100644
index 0000000..33defed
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+      {
+          "name": "DynamicInstrumentationManagerServiceTests"
+      }
+  ]
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java
new file mode 100644
index 0000000..04073fa
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClass.java
@@ -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.server;
+
+public abstract class TestAbstractClass {
+    abstract void abstractMethod();
+
+    void concreteMethod() {
+    }
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java
new file mode 100644
index 0000000..2c25e7a5
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestAbstractClassImpl.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class TestAbstractClassImpl extends TestAbstractClass {
+    @Override
+    void abstractMethod() {
+    }
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java
new file mode 100644
index 0000000..085f595
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestClass.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+public class TestClass {
+    void primitiveParams(boolean a, boolean[] b, byte c, byte[] d, char e, char[] f, short g,
+            short[] h, int i, int[] j, long k, long[] l, float m, float[] n, double o, double[] p) {
+    }
+
+    void classParams(String a, String[] b) {
+    }
+
+    private void privateMethod() {
+    }
+
+    /**
+     * docs!
+     */
+    public void publicMethod() {
+    }
+
+    private static class InnerClass {
+        private void innerMethod(InnerClass arg, InnerClass[] argArray) {
+        }
+    }
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java
new file mode 100644
index 0000000..7af4f25
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterface.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * docs!
+ */
+public interface TestInterface {
+    /**
+     * docs!
+     */
+    void interfaceMethod();
+
+    /**
+     * docs!
+     */
+    default void defaultMethod() {
+    }
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java
new file mode 100644
index 0000000..53aecbc
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/TestInterfaceImpl.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class TestInterfaceImpl implements TestInterface {
+    @Override
+    public void interfaceMethod() {
+    }
+}
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
new file mode 100644
index 0000000..5492ba6
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/src/com/android/server/os/instrumentation/ParseMethodDescriptorTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.os.instrumentation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.os.instrumentation.MethodDescriptor;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.TestAbstractClass;
+import com.android.server.TestAbstractClassImpl;
+import com.android.server.TestClass;
+import com.android.server.TestInterface;
+import com.android.server.TestInterfaceImpl;
+
+import org.junit.Test;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * Test class for
+ * {@link DynamicInstrumentationManagerService#parseMethodDescriptor(ClassLoader,
+ * MethodDescriptor)}.
+ * <p>
+ * Build/Install/Run:
+ * atest FrameworksMockingServicesTests:ParseMethodDescriptorTest
+ */
+@Presubmit
+@SmallTest
+public class ParseMethodDescriptorTest {
+    private static final String[] PRIMITIVE_PARAMS = new String[]{
+            "boolean", "boolean[]", "byte", "byte[]", "char", "char[]", "short", "short[]", "int",
+            "int[]", "long", "long[]", "float", "float[]", "double", "double[]"};
+    private static final String[] CLASS_PARAMS =
+            new String[]{"java.lang.String", "java.lang.String[]"};
+
+    @Test
+    public void primitiveParams() {
+        assertNotNull(parseMethodDescriptor(TestClass.class.getName(), "primitiveParams",
+                PRIMITIVE_PARAMS));
+    }
+
+    @Test
+    public void classParams() {
+        assertNotNull(
+                parseMethodDescriptor(TestClass.class.getName(), "classParams", CLASS_PARAMS));
+    }
+
+    @Test
+    public void publicMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestClass.class.getName(), "publicMethod"));
+    }
+
+    @Test
+    public void privateMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestClass.class.getName(), "privateMethod"));
+    }
+
+    @Test
+    public void innerClass() {
+        assertNotNull(
+                parseMethodDescriptor(TestClass.class.getName() + "$InnerClass", "innerMethod",
+                        new String[]{TestClass.class.getName() + "$InnerClass",
+                                TestClass.class.getName() + "$InnerClass[]"}));
+    }
+
+    @Test
+    public void interface_concreteMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestInterfaceImpl.class.getName(), "interfaceMethod"));
+    }
+
+    @Test
+    public void interface_defaultMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestInterface.class.getName(), "defaultMethod"));
+    }
+
+    @Test
+    public void abstractClassImpl_abstractMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestAbstractClassImpl.class.getName(), "abstractMethod"));
+    }
+
+    @Test
+    public void abstractClass_concreteMethod() {
+        assertNotNull(
+                parseMethodDescriptor(TestAbstractClass.class.getName(), "concreteMethod"));
+    }
+
+    @Test
+    public void notFound_illegalArgumentException() {
+        assertThrows(IllegalArgumentException.class, () -> parseMethodDescriptor("foo", "bar"));
+        assertThrows(IllegalArgumentException.class,
+                () -> parseMethodDescriptor(TestClass.class.getName(), "bar"));
+        assertThrows(IllegalArgumentException.class,
+                () -> parseMethodDescriptor(TestClass.class.getName(), "primitiveParams",
+                        new String[]{"int"}));
+    }
+
+    private Method parseMethodDescriptor(String fqcn, String methodName) {
+        return DynamicInstrumentationManagerService.parseMethodDescriptor(
+                getClass().getClassLoader(),
+                getMethodDescriptor(fqcn, methodName, new String[]{}));
+    }
+
+    private Method parseMethodDescriptor(String fqcn, String methodName, String[] fqParameters) {
+        return DynamicInstrumentationManagerService.parseMethodDescriptor(
+                getClass().getClassLoader(),
+                getMethodDescriptor(fqcn, methodName, fqParameters));
+    }
+
+    private MethodDescriptor getMethodDescriptor(String fqcn, String methodName,
+            String[] fqParameters) {
+        MethodDescriptor methodDescriptor = new MethodDescriptor();
+        methodDescriptor.fullyQualifiedClassName = fqcn;
+        methodDescriptor.methodName = methodName;
+        methodDescriptor.fullyQualifiedParameters = fqParameters;
+        return methodDescriptor;
+    }
+
+
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
new file mode 100644
index 0000000..1be5cef
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.pm;
+
+import static android.content.pm.Flags.FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.AppModeNonSdkSandbox;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.pm.parsing.pkg.AndroidPackageInternal;
+import com.android.internal.pm.pkg.component.ParsedActivity;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@AppModeFull
+@AppModeNonSdkSandbox
+@RunWith(AndroidJUnit4.class)
+public class BroadcastHelperTest {
+    private static final String TAG = "BroadcastHelperTest";
+    private static final String PACKAGE_CHANGED_TEST_PACKAGE_NAME = "testpackagename";
+    private static final String PACKAGE_CHANGED_TEST_MAIN_ACTIVITY =
+            PACKAGE_CHANGED_TEST_PACKAGE_NAME + ".MainActivity";
+    private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED =
+            "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED";
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    ActivityManagerInternal mMockActivityManagerInternal;
+    @Mock
+    AndroidPackageInternal mMockAndroidPackageInternal;
+    @Mock
+    Computer mMockSnapshot;
+    @Mock
+    Handler mMockHandler;
+    @Mock
+    PackageManagerServiceInjector mMockPackageManagerServiceInjector;
+    @Mock
+    PackageMonitorCallbackHelper mMockPackageMonitorCallbackHelper;
+    @Mock
+    PackageStateInternal mMockPackageStateInternal;
+    @Mock
+    ParsedActivity mMockParsedActivity;
+    @Mock
+    UserManagerInternal mMockUserManagerInternal;
+
+    private Context mContext;
+    private BroadcastHelper mBroadcastHelper;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+        when(mMockHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+                i -> {
+                    ((Message) i.getArguments()[0]).getCallback().run();
+                    return true;
+                });
+        when(mMockPackageManagerServiceInjector.getActivityManagerInternal()).thenReturn(
+                mMockActivityManagerInternal);
+        when(mMockPackageManagerServiceInjector.getContext()).thenReturn(mContext);
+        when(mMockPackageManagerServiceInjector.getHandler()).thenReturn(mMockHandler);
+        when(mMockPackageManagerServiceInjector.getPackageMonitorCallbackHelper()).thenReturn(
+                mMockPackageMonitorCallbackHelper);
+        when(mMockPackageManagerServiceInjector.getUserManagerInternal()).thenReturn(
+                mMockUserManagerInternal);
+
+        mBroadcastHelper = new BroadcastHelper(mMockPackageManagerServiceInjector);
+    }
+
+    @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+    @Test
+    public void changeNonExportedComponent_sendPackageChangedBroadcastToSystem_withPermission()
+            throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(
+                captor.capture(), eq(null),
+                eq(new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}),
+                anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertThat(intent.getPackage()).isEqualTo("android");
+    }
+
+    @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+    @Test
+    public void changeNonExportedComponent_sendPackageChangedBroadcastToApplicationItself()
+            throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+                eq(null), anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertThat(intent.getPackage()).isEqualTo(PACKAGE_CHANGED_TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void changeExportedComponent_sendPackageChangedBroadcastToAll() throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(true /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+                eq(null), anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertNull(intent.getPackage());
+    }
+
+    private void changeComponentAndSendPackageChangedBroadcast(boolean changeExportedComponent) {
+        when(mMockSnapshot.getPackageStateInternal(eq(PACKAGE_CHANGED_TEST_PACKAGE_NAME),
+                anyInt())).thenReturn(mMockPackageStateInternal);
+        when(mMockSnapshot.isInstantAppInternal(any(), anyInt(), anyInt())).thenReturn(false);
+        when(mMockSnapshot.getVisibilityAllowLists(any(), any())).thenReturn(null);
+        when(mMockPackageStateInternal.getPkg()).thenReturn(mMockAndroidPackageInternal);
+
+        when(mMockParsedActivity.getClassName()).thenReturn(
+                PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+        when(mMockParsedActivity.isExported()).thenReturn(changeExportedComponent);
+        ArrayList<ParsedActivity> parsedActivities = new ArrayList<>();
+        parsedActivities.add(mMockParsedActivity);
+
+        when(mMockAndroidPackageInternal.getReceivers()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getProviders()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getServices()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getActivities()).thenReturn(parsedActivities);
+
+        doNothing().when(mMockPackageMonitorCallbackHelper).notifyPackageChanged(any(),
+                anyBoolean(), any(), anyInt(), any(), any(), any(), any(), any());
+        when(mMockActivityManagerInternal.broadcastIntentWithCallback(any(), any(), any(), anyInt(),
+                any(), any(), any())).thenReturn(ActivityManager.BROADCAST_SUCCESS);
+
+        ArrayList<String> componentNames = new ArrayList<>();
+        componentNames.add(PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+
+        mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot,
+                PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames,
+                UserHandle.USER_SYSTEM, "test" /* reason */);
+    }
+}
diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
index 37a34ee..205ff05 100644
--- a/services/tests/displayservicetests/AndroidManifest.xml
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -29,7 +29,6 @@
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.MANAGE_USB" />
-    <uses-permission android:name="android.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE" />
 
     <!-- Permissions needed for DisplayTransformManagerTest -->
     <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index c741cae..80e5ee3 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -19,13 +19,16 @@
 import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
 import static android.Manifest.permission.CAPTURE_VIDEO_OUTPUT;
+import static android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS;
 import static android.Manifest.permission.MANAGE_DISPLAYS;
+import static android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -96,6 +99,7 @@
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayOffloader;
+import android.hardware.display.DisplayTopology;
 import android.hardware.display.DisplayViewport;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -111,11 +115,13 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.MessageQueue;
+import android.os.PermissionEnforcer;
 import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserManager;
+import android.os.test.FakePermissionEnforcer;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
@@ -251,6 +257,8 @@
 
     private int[] mAllowedHdrOutputTypes;
 
+    private final FakePermissionEnforcer mPermissionEnforcer = new FakePermissionEnforcer();
+
     private final DisplayManagerService.Injector mShortMockedInjector =
             new DisplayManagerService.Injector() {
                 @Override
@@ -428,6 +436,13 @@
         when(mContext.getResources()).thenReturn(mResources);
         mUserManager = Mockito.spy(mContext.getSystemService(UserManager.class));
 
+        mPermissionEnforcer.grant(CONTROL_DISPLAY_BRIGHTNESS);
+        mPermissionEnforcer.grant(MODIFY_USER_PREFERRED_DISPLAY_MODE);
+        doReturn(Context.PERMISSION_ENFORCER_SERVICE).when(mContext).getSystemServiceName(
+                eq(PermissionEnforcer.class));
+        doReturn(mPermissionEnforcer).when(mContext).getSystemService(
+                eq(Context.PERMISSION_ENFORCER_SERVICE));
+
         VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext);
         when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
         when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
@@ -3667,6 +3682,87 @@
         verify(mMockVirtualDisplayAdapter).releaseVirtualDisplayLocked(binder, callingUid);
     }
 
+    @Test
+    public void testGetDisplayTopology() {
+        Settings.Global.putInt(mContext.getContentResolver(),
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 1);
+        manageDisplaysPermission(/* granted= */ true);
+        when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true);
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        initDisplayPowerController(localService);
+
+        DisplayTopology topology = displayManagerBinderService.getDisplayTopology();
+        assertNotNull(topology);
+        DisplayTopology.TreeNode display = topology.getRoot();
+        assertNotNull(display);
+        assertEquals(Display.DEFAULT_DISPLAY, display.getDisplayId());
+    }
+
+    @Test
+    public void testGetDisplayTopology_NullIfFlagDisabled() {
+        manageDisplaysPermission(/* granted= */ true);
+        when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(false);
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        initDisplayPowerController(localService);
+
+        DisplayTopology topology = displayManagerBinderService.getDisplayTopology();
+        assertNull(topology);
+    }
+
+    @Test
+    public void testGetDisplayTopology_withoutPermission_shouldThrowException() {
+        when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true);
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        initDisplayPowerController(localService);
+
+        assertThrows(SecurityException.class, displayManagerBinderService::getDisplayTopology);
+    }
+
+    @Test
+    public void testSetDisplayTopology() {
+        manageDisplaysPermission(/* granted= */ true);
+        when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true);
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        initDisplayPowerController(localService);
+
+        displayManagerBinderService.setDisplayTopology(new DisplayTopology());
+    }
+
+    @Test
+    public void testSetDisplayTopology_withoutPermission_shouldThrowException() {
+        when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true);
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        initDisplayPowerController(localService);
+
+        assertThrows(SecurityException.class,
+                () -> displayManagerBinderService.setDisplayTopology(new DisplayTopology()));
+    }
+
     private void initDisplayPowerController(DisplayManagerInternal localService) {
         localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() {
             @Override
@@ -3850,6 +3946,10 @@
         DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
         displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
         displayDeviceInfo.modeId = modeId;
+        if (modeId > 0 && modeId <= displayDeviceInfo.supportedModes.length) {
+            displayDeviceInfo.renderFrameRate =
+                displayDeviceInfo.supportedModes[modeId - 1].getRefreshRate();
+        }
         updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
     }
 
@@ -4017,9 +4117,11 @@
     private void manageDisplaysPermission(boolean granted) {
         if (granted) {
             doNothing().when(mContext).enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any());
+            mPermissionEnforcer.grant(MANAGE_DISPLAYS);
         } else {
             doThrow(new SecurityException("MANAGE_DISPLAYS permission denied")).when(mContext)
                     .enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any());
+            mPermissionEnforcer.revoke(MANAGE_DISPLAYS);
         }
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
index 85e7356..a2d2a81 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.display
 
+import android.hardware.display.DisplayTopology
 import android.util.DisplayMetrics
 import android.view.Display
 import android.view.DisplayInfo
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt
deleted file mode 100644
index cd8c26d..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt
+++ /dev/null
@@ -1,476 +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.server.display
-
-import android.view.Display
-import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_BOTTOM
-import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_TOP
-import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_RIGHT
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class DisplayTopologyTest {
-    private val topology = DisplayTopology()
-
-    @Test
-    fun addOneDisplay() {
-        val displayId = 1
-        val width = 800f
-        val height = 600f
-
-        topology.addDisplay(displayId, width, height)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId)
-
-        val display = topology.mRoot!!
-        assertThat(display.mDisplayId).isEqualTo(displayId)
-        assertThat(display.mWidth).isEqualTo(width)
-        assertThat(display.mHeight).isEqualTo(height)
-        assertThat(display.mChildren).isEmpty()
-    }
-
-    @Test
-    fun addTwoDisplays() {
-        val displayId1 = 1
-        val width1 = 800f
-        val height1 = 600f
-
-        val displayId2 = 2
-        val width2 = 1000f
-        val height2 = 1500f
-
-        topology.addDisplay(displayId1, width1, height1)
-        topology.addDisplay(displayId2, width2, height2)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
-
-        val display1 = topology.mRoot!!
-        assertThat(display1.mDisplayId).isEqualTo(displayId1)
-        assertThat(display1.mWidth).isEqualTo(width1)
-        assertThat(display1.mHeight).isEqualTo(height1)
-        assertThat(display1.mChildren).hasSize(1)
-
-        val display2 = display1.mChildren[0]
-        assertThat(display2.mDisplayId).isEqualTo(displayId2)
-        assertThat(display2.mWidth).isEqualTo(width2)
-        assertThat(display2.mHeight).isEqualTo(height2)
-        assertThat(display2.mChildren).isEmpty()
-        assertThat(display2.mPosition).isEqualTo(POSITION_TOP)
-        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
-    }
-
-    @Test
-    fun addManyDisplays() {
-        val displayId1 = 1
-        val width1 = 800f
-        val height1 = 600f
-
-        val displayId2 = 2
-        val width2 = 1000f
-        val height2 = 1500f
-
-        topology.addDisplay(displayId1, width1, height1)
-        topology.addDisplay(displayId2, width2, height2)
-
-        val noOfDisplays = 30
-        for (i in 3..noOfDisplays) {
-            topology.addDisplay(/* displayId= */ i, width1, height1)
-        }
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
-
-        val display1 = topology.mRoot!!
-        assertThat(display1.mDisplayId).isEqualTo(displayId1)
-        assertThat(display1.mWidth).isEqualTo(width1)
-        assertThat(display1.mHeight).isEqualTo(height1)
-        assertThat(display1.mChildren).hasSize(1)
-
-        val display2 = display1.mChildren[0]
-        assertThat(display2.mDisplayId).isEqualTo(displayId2)
-        assertThat(display2.mWidth).isEqualTo(width2)
-        assertThat(display2.mHeight).isEqualTo(height2)
-        assertThat(display2.mChildren).hasSize(1)
-        assertThat(display2.mPosition).isEqualTo(POSITION_TOP)
-        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
-
-        var display = display2
-        for (i in 3..noOfDisplays) {
-            display = display.mChildren[0]
-            assertThat(display.mDisplayId).isEqualTo(i)
-            assertThat(display.mWidth).isEqualTo(width1)
-            assertThat(display.mHeight).isEqualTo(height1)
-            // The last display should have no children
-            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
-            assertThat(display.mPosition).isEqualTo(POSITION_RIGHT)
-            assertThat(display.mOffset).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun removeDisplays() {
-        val displayId1 = 1
-        val width1 = 800f
-        val height1 = 600f
-
-        val displayId2 = 2
-        val width2 = 1000f
-        val height2 = 1500f
-
-        topology.addDisplay(displayId1, width1, height1)
-        topology.addDisplay(displayId2, width2, height2)
-
-        val noOfDisplays = 30
-        for (i in 3..noOfDisplays) {
-            topology.addDisplay(/* displayId= */ i, width1, height1)
-        }
-
-        var removedDisplays = arrayOf(20)
-        topology.removeDisplay(20)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
-
-        var display1 = topology.mRoot!!
-        assertThat(display1.mDisplayId).isEqualTo(displayId1)
-        assertThat(display1.mWidth).isEqualTo(width1)
-        assertThat(display1.mHeight).isEqualTo(height1)
-        assertThat(display1.mChildren).hasSize(1)
-
-        var display2 = display1.mChildren[0]
-        assertThat(display2.mDisplayId).isEqualTo(displayId2)
-        assertThat(display2.mWidth).isEqualTo(width2)
-        assertThat(display2.mHeight).isEqualTo(height2)
-        assertThat(display2.mChildren).hasSize(1)
-        assertThat(display2.mPosition).isEqualTo(POSITION_TOP)
-        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
-
-        var display = display2
-        for (i in 3..noOfDisplays) {
-            if (i in removedDisplays) {
-                continue
-            }
-            display = display.mChildren[0]
-            assertThat(display.mDisplayId).isEqualTo(i)
-            assertThat(display.mWidth).isEqualTo(width1)
-            assertThat(display.mHeight).isEqualTo(height1)
-            // The last display should have no children
-            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
-            assertThat(display.mPosition).isEqualTo(POSITION_RIGHT)
-            assertThat(display.mOffset).isEqualTo(0)
-        }
-
-        topology.removeDisplay(22)
-        removedDisplays += 22
-        topology.removeDisplay(23)
-        removedDisplays += 23
-        topology.removeDisplay(25)
-        removedDisplays += 25
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
-
-        display1 = topology.mRoot!!
-        assertThat(display1.mDisplayId).isEqualTo(displayId1)
-        assertThat(display1.mWidth).isEqualTo(width1)
-        assertThat(display1.mHeight).isEqualTo(height1)
-        assertThat(display1.mChildren).hasSize(1)
-
-        display2 = display1.mChildren[0]
-        assertThat(display2.mDisplayId).isEqualTo(displayId2)
-        assertThat(display2.mWidth).isEqualTo(width2)
-        assertThat(display2.mHeight).isEqualTo(height2)
-        assertThat(display2.mChildren).hasSize(1)
-        assertThat(display2.mPosition).isEqualTo(POSITION_TOP)
-        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)
-
-        display = display2
-        for (i in 3..noOfDisplays) {
-            if (i in removedDisplays) {
-                continue
-            }
-            display = display.mChildren[0]
-            assertThat(display.mDisplayId).isEqualTo(i)
-            assertThat(display.mWidth).isEqualTo(width1)
-            assertThat(display.mHeight).isEqualTo(height1)
-            // The last display should have no children
-            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
-            assertThat(display.mPosition).isEqualTo(POSITION_RIGHT)
-            assertThat(display.mOffset).isEqualTo(0)
-        }
-    }
-
-    @Test
-    fun removeAllDisplays() {
-        val displayId = 1
-        val width = 800f
-        val height = 600f
-
-        topology.addDisplay(displayId, width, height)
-        topology.removeDisplay(displayId)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(Display.INVALID_DISPLAY)
-        assertThat(topology.mRoot).isNull()
-    }
-
-    @Test
-    fun removeDisplayThatDoesNotExist() {
-        val displayId = 1
-        val width = 800f
-        val height = 600f
-
-        topology.addDisplay(displayId, width, height)
-        topology.removeDisplay(3)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId)
-
-        val display = topology.mRoot!!
-        assertThat(display.mDisplayId).isEqualTo(displayId)
-        assertThat(display.mWidth).isEqualTo(width)
-        assertThat(display.mHeight).isEqualTo(height)
-        assertThat(display.mChildren).isEmpty()
-    }
-
-    @Test
-    fun removePrimaryDisplay() {
-        val displayId1 = 1
-        val displayId2 = 2
-        val width = 800f
-        val height = 600f
-
-        topology.addDisplay(displayId1, width, height)
-        topology.addDisplay(displayId2, width, height)
-        topology.mPrimaryDisplayId = displayId2
-        topology.removeDisplay(displayId2)
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)
-        val display = topology.mRoot!!
-        assertThat(display.mDisplayId).isEqualTo(displayId1)
-        assertThat(display.mWidth).isEqualTo(width)
-        assertThat(display.mHeight).isEqualTo(height)
-        assertThat(display.mChildren).isEmpty()
-    }
-
-    @Test
-    fun normalization_noOverlaps_leavesTopologyUnchanged() {
-        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
-            /* height= */ 600f, /* position= */ null, /* offset= */ 0f)
-        topology.mRoot = display1
-
-        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f)
-        display1.mChildren.add(display2)
-
-        val primaryDisplayId = 3
-        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f)
-        display1.mChildren.add(display3)
-        topology.mPrimaryDisplayId = primaryDisplayId
-
-        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
-            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
-        display2.mChildren.add(display4)
-
-        topology.normalize()
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId)
-
-        val actualDisplay1 = topology.mRoot!!
-        assertThat(actualDisplay1.mDisplayId).isEqualTo(1)
-        assertThat(actualDisplay1.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay1.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay1.mChildren).hasSize(2)
-
-        val actualDisplay2 = actualDisplay1.mChildren[0]
-        assertThat(actualDisplay2.mDisplayId).isEqualTo(2)
-        assertThat(actualDisplay2.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay2.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay2.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay2.mChildren).hasSize(1)
-
-        val actualDisplay3 = actualDisplay1.mChildren[1]
-        assertThat(actualDisplay3.mDisplayId).isEqualTo(3)
-        assertThat(actualDisplay3.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay3.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay3.mOffset).isEqualTo(400f)
-        assertThat(actualDisplay3.mChildren).isEmpty()
-
-        val actualDisplay4 = actualDisplay2.mChildren[0]
-        assertThat(actualDisplay4.mDisplayId).isEqualTo(4)
-        assertThat(actualDisplay4.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay4.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay4.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay4.mChildren).isEmpty()
-    }
-
-    @Test
-    fun normalization_moveDisplayWithoutReparenting() {
-        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
-            /* height= */ 600f, /* position= */ null, /* offset= */ 0f)
-        topology.mRoot = display1
-
-        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f,
-            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
-        display1.mChildren.add(display2)
-
-        val primaryDisplayId = 3
-        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f)
-        display1.mChildren.add(display3)
-        topology.mPrimaryDisplayId = primaryDisplayId
-
-        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
-            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
-        display2.mChildren.add(display4)
-
-        // Display 3 becomes a child of display 2. Display 4 gets moved without changing its parent.
-        topology.normalize()
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId)
-
-        val actualDisplay1 = topology.mRoot!!
-        assertThat(actualDisplay1.mDisplayId).isEqualTo(1)
-        assertThat(actualDisplay1.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay1.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay1.mChildren).hasSize(1)
-
-        val actualDisplay2 = actualDisplay1.mChildren[0]
-        assertThat(actualDisplay2.mDisplayId).isEqualTo(2)
-        assertThat(actualDisplay2.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay2.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay2.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay2.mChildren).hasSize(2)
-
-        val actualDisplay3 = actualDisplay2.mChildren[1]
-        assertThat(actualDisplay3.mDisplayId).isEqualTo(3)
-        assertThat(actualDisplay3.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay3.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay3.mOffset).isEqualTo(10f)
-        assertThat(actualDisplay3.mChildren).isEmpty()
-
-        val actualDisplay4 = actualDisplay2.mChildren[0]
-        assertThat(actualDisplay4.mDisplayId).isEqualTo(4)
-        assertThat(actualDisplay4.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay4.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay4.mOffset).isEqualTo(210f)
-        assertThat(actualDisplay4.mChildren).isEmpty()
-    }
-
-    @Test
-    fun normalization_moveDisplayWithoutReparenting_offsetOutOfBounds() {
-        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
-            /* height= */ 50f, /* position= */ null, /* offset= */ 0f)
-        topology.mRoot = display1
-
-        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f)
-        display1.mChildren.add(display2)
-
-        val primaryDisplayId = 3
-        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f)
-        display1.mChildren.add(display3)
-        topology.mPrimaryDisplayId = primaryDisplayId
-
-        // Display 3 gets moved and its left side is still on the same line as the right side
-        // of Display 1, but it no longer touches it (the offset is out of bounds), so Display 2
-        // becomes its new parent.
-        topology.normalize()
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId)
-
-        val actualDisplay1 = topology.mRoot!!
-        assertThat(actualDisplay1.mDisplayId).isEqualTo(1)
-        assertThat(actualDisplay1.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay1.mHeight).isEqualTo(50f)
-        assertThat(actualDisplay1.mChildren).hasSize(1)
-
-        val actualDisplay2 = actualDisplay1.mChildren[0]
-        assertThat(actualDisplay2.mDisplayId).isEqualTo(2)
-        assertThat(actualDisplay2.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay2.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay2.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay2.mChildren).hasSize(1)
-
-        val actualDisplay3 = actualDisplay2.mChildren[0]
-        assertThat(actualDisplay3.mDisplayId).isEqualTo(3)
-        assertThat(actualDisplay3.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay3.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_BOTTOM)
-        assertThat(actualDisplay3.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay3.mChildren).isEmpty()
-    }
-
-    @Test
-    fun normalization_moveAndReparentDisplay() {
-        val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
-            /* height= */ 600f, /* position= */ null, /* offset= */ 0f)
-        topology.mRoot = display1
-
-        val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f,
-            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
-        display1.mChildren.add(display2)
-
-        val primaryDisplayId = 3
-        val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
-            /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f)
-        display1.mChildren.add(display3)
-        topology.mPrimaryDisplayId = primaryDisplayId
-
-        val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
-            /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f)
-        display2.mChildren.add(display4)
-
-        topology.normalize()
-
-        assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId)
-
-        val actualDisplay1 = topology.mRoot!!
-        assertThat(actualDisplay1.mDisplayId).isEqualTo(1)
-        assertThat(actualDisplay1.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay1.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay1.mChildren).hasSize(1)
-
-        val actualDisplay2 = actualDisplay1.mChildren[0]
-        assertThat(actualDisplay2.mDisplayId).isEqualTo(2)
-        assertThat(actualDisplay2.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay2.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay2.mOffset).isEqualTo(0f)
-        assertThat(actualDisplay2.mChildren).hasSize(1)
-
-        val actualDisplay3 = actualDisplay2.mChildren[0]
-        assertThat(actualDisplay3.mDisplayId).isEqualTo(3)
-        assertThat(actualDisplay3.mWidth).isEqualTo(600f)
-        assertThat(actualDisplay3.mHeight).isEqualTo(200f)
-        assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay3.mOffset).isEqualTo(400f)
-        assertThat(actualDisplay3.mChildren).hasSize(1)
-
-        val actualDisplay4 = actualDisplay3.mChildren[0]
-        assertThat(actualDisplay4.mDisplayId).isEqualTo(4)
-        assertThat(actualDisplay4.mWidth).isEqualTo(200f)
-        assertThat(actualDisplay4.mHeight).isEqualTo(600f)
-        assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT)
-        assertThat(actualDisplay4.mOffset).isEqualTo(-400f)
-        assertThat(actualDisplay4.mChildren).isEmpty()
-    }
-}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index be698b2..979384c6 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -134,6 +134,7 @@
         "androidx.annotation_annotation",
         "androidx.test.rules",
         "services.core",
+        "servicestests-utils-mockito-extended",
     ],
     srcs: [
         "src/com/android/server/am/BroadcastRecordTest.java",
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index d602660..a1a8b0e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -41,6 +41,7 @@
 import android.os.UserHandle;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.util.SparseArray;
 
@@ -97,6 +98,9 @@
             .spyStatic(ProcessList.class)
             .build();
 
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
@@ -112,6 +116,8 @@
     AlarmManagerInternal mAlarmManagerInt;
     @Mock
     ProcessList mProcessList;
+    @Mock
+    PlatformCompat mPlatformCompat;
 
     @Mock
     AppStartInfoTracker mAppStartInfoTracker;
@@ -178,6 +184,11 @@
         doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
 
         doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
+
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     public void tearDown() throws Exception {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 100b548..1481085 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
@@ -49,7 +50,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -65,7 +65,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.BundleMerger;
@@ -73,6 +72,8 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
 
@@ -182,10 +183,6 @@
         return mock(Intent.class);
     }
 
-    private static ResolveInfo makeMockManifestReceiver() {
-        return mock(ResolveInfo.class);
-    }
-
     private static BroadcastFilter makeMockRegisteredReceiver() {
         return mock(BroadcastFilter.class);
     }
@@ -214,7 +211,8 @@
         return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false,
                 null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
                 Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
-                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
+                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
@@ -646,7 +644,8 @@
     @Test
     public void testRunnableAt_Cached_Manifest() {
         doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
-                List.of(makeMockManifestReceiver()), null, false), REASON_CONTAINS_MANIFEST);
+                List.of(makeManifestReceiver(PACKAGE_RED, CLASS_RED)), null, false),
+                REASON_CONTAINS_MANIFEST);
     }
 
     @Test
@@ -679,6 +678,19 @@
                 List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_ALARM);
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testRunnableAt_Cached_Prioritized_NonDeferrable_flagDisabled() {
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, null, false), REASON_CONTAINS_PRIORITIZED);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
     public void testRunnableAt_Cached_Prioritized_NonDeferrable() {
         final List receivers = List.of(
@@ -687,6 +699,32 @@
         final BroadcastOptions options = BroadcastOptions.makeBasic()
                 .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
         doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, null, false), REASON_CONTAINS_MANIFEST);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Ordered_NonDeferrable() {
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, mock(IIntentReceiver.class), true), REASON_CONTAINS_ORDERED);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
                 receivers, null, false), REASON_CONTAINS_PRIORITIZED);
     }
 
@@ -1136,6 +1174,63 @@
         verifyPendingRecords(blueQueue, List.of(screenOn));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled() {
+        final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
+        final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF);
+        final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic()
+                .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+                .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON);
+
+        final Object greenReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10);
+        final Object redReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5);
+        final Object blueReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0);
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE));
+        verifyPendingRecords(greenQueue, List.of(screenOff));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff));
+
+        assertTrue(greenQueue.isEmpty());
+        assertTrue(redQueue.isEmpty());
+        assertTrue(blueQueue.isEmpty());
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff, screenOn));
+
+        final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false);
+        screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED,
+                "testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled");
+        mImpl.enqueueBroadcastLocked(screenOffRecord);
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
     @Test
     public void testDeliveryGroupPolicy_prioritized_diffReceivers() {
         final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
@@ -1173,6 +1268,65 @@
                 List.of(greenReceiver, redReceiver, blueReceiver), false));
         mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
                 List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+
+        final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false);
+        screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED,
+                "testDeliveryGroupPolicy_prioritized_diffReceivers");
+        mImpl.enqueueBroadcastLocked(screenOffRecord);
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+
+        final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
+        final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF);
+        final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic()
+                .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+                .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON);
+
+        final Object greenReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10);
+        final Object redReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5);
+        final Object blueReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0);
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE));
+        verifyPendingRecords(greenQueue, List.of(screenOff));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff));
+
+        assertTrue(greenQueue.isEmpty());
+        assertTrue(redQueue.isEmpty());
+        assertTrue(blueQueue.isEmpty());
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
         verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
         verifyPendingRecords(redQueue, List.of(screenOff));
         verifyPendingRecords(blueQueue, List.of(screenOff, screenOn));
@@ -1569,8 +1723,9 @@
         verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
-    public void testDeliveryDeferredForCached() throws Exception {
+    public void testDeliveryDeferredForCached_flagDisabled() throws Exception {
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
         final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
 
@@ -1664,8 +1819,217 @@
         }, false /* andRemove */);
     }
 
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
     @Test
-    public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception {
+    public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred_flagDisabled() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastOptions optionsPackageChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                optionsPackageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled()
+            throws Exception {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
         final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 3aaf2e5..9d92d5f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -21,6 +21,7 @@
 import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER;
 import static android.os.UserHandle.USER_SYSTEM;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO;
 import static com.android.server.am.BroadcastProcessQueue.reasonToString;
 import static com.android.server.am.BroadcastRecord.deliveryStateToString;
@@ -45,7 +46,6 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -77,6 +77,8 @@
 import android.os.PowerExemptionManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -446,7 +448,8 @@
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
                 AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo,
                 Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId,
-                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
+                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private void assertHealth() {
@@ -1495,7 +1498,7 @@
                 null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(),
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, null,
                 Activity.RESULT_OK, null, null, false, false, false, UserHandle.USER_SYSTEM,
-                backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN);
+                backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN, mPlatformCompat);
         enqueueBroadcast(r);
 
         waitForIdle();
@@ -1550,8 +1553,10 @@
     /**
      * Verify that when dispatching we respect tranches of priority.
      */
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("DistinctVarargsChecker")
     @Test
-    public void testPriority() throws Exception {
+    public void testPriority_flagDisabled() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -1594,6 +1599,106 @@
     }
 
     /**
+     * Verify that when dispatching we respect tranches of priority.
+     */
+    @SuppressWarnings("DistinctVarargsChecker")
+    @Test
+    public void testOrdered_withPriorities() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        // Enqueue a normal broadcast that will go to several processes, and
+        // then enqueue a foreground broadcast that risks reordering
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class);
+        enqueueBroadcast(makeOrderedBroadcastRecord(timezone, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp, 10),
+                        makeRegisteredReceiver(receiverGreenApp, 10),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW),
+                        makeRegisteredReceiver(receiverYellowApp, -10)),
+                orderedResultTo, null));
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp))));
+        waitForIdle();
+
+        // Ignore the final foreground broadcast
+        mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane));
+        assertEquals(6, mScheduledBroadcasts.size());
+
+        // We're only concerned about enforcing ordering between tranches;
+        // within a tranche we're okay with reordering
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverGreenApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0)));
+    }
+
+    /**
+     * Verify that when dispatching we respect tranches of priority.
+     */
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("DistinctVarargsChecker")
+    @Test
+    public void testPriority_changeIdDisabled() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+
+        // Enqueue a normal broadcast that will go to several processes, and
+        // then enqueue a foreground broadcast that risks reordering
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp, 10),
+                        makeRegisteredReceiver(receiverGreenApp, 10),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW),
+                        makeRegisteredReceiver(receiverYellowApp, -10))));
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp))));
+        waitForIdle();
+
+        // Ignore the final foreground broadcast
+        mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane));
+        assertEquals(5, mScheduledBroadcasts.size());
+
+        // We're only concerned about enforcing ordering between tranches;
+        // within a tranche we're okay with reordering
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverGreenApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0)));
+    }
+
+    /**
      * Verify prioritized receivers work as expected with deferrable broadcast - broadcast to
      * app in cached state should be deferred and the rest should be delivered as per the priority
      * order.
@@ -2305,8 +2410,9 @@
                 .isLessThan(getReceiverScheduledTime(timeTickRecord, receiverBlue));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
-    public void testPrioritizedBroadcastDelivery_uidForeground() throws Exception {
+    public void testPrioritizedBroadcastDelivery_uidForeground_flagDisabled() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -2332,6 +2438,63 @@
     }
 
     @Test
+    public void testOrderedBroadcastDelivery_uidForeground() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
+                ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10);
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5);
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        final BroadcastRecord prioritizedRecord = makeOrderedBroadcastRecord(timeTick, callerApp,
+                List.of(receiverBlue, receiverGreen), resultTo, null);
+
+        enqueueBroadcast(prioritizedRecord);
+
+        waitForIdle();
+        // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast.
+        // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp.
+        assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen))
+                .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testPrioritizedBroadcastDelivery_uidForeground_changeIdDisabled() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+
+        mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
+                ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10);
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5);
+        final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp,
+                List.of(receiverBlue, receiverGreen));
+
+        enqueueBroadcast(prioritizedRecord);
+
+        waitForIdle();
+        // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast.
+        // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp.
+        assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen))
+                .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue));
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS)
     public void testDeferOutgoingBroadcasts() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
index 8cd0da7..4a370a3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING;
@@ -33,6 +35,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
@@ -46,11 +50,17 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telephony.SubscriptionManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.compat.PlatformCompat;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -73,6 +83,9 @@
 public class BroadcastRecordTest {
     private static final String TAG = "BroadcastRecordTest";
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final int USER0 = UserHandle.USER_SYSTEM;
     private static final String PACKAGE1 = "pkg1";
     private static final String PACKAGE2 = "pkg2";
@@ -89,10 +102,14 @@
 
     @Mock BroadcastQueue mQueue;
     @Mock ProcessRecord mProcess;
+    @Mock PlatformCompat mPlatformCompat;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     @Test
@@ -108,13 +125,13 @@
 
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), 0)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), 0)), false, mPlatformCompat));
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), -10)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), -10)), false, mPlatformCompat));
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), 10)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), 10)), false, mPlatformCompat));
     }
 
     @Test
@@ -128,18 +145,19 @@
                 createResolveInfo(PACKAGE2, getAppId(2), 10),
                 createResolveInfo(PACKAGE3, getAppId(3), 10))));
 
-        assertArrayEquals(new int[] {-1,-1,-1},
+        assertArrayEquals(new int[] {-1, -1, -1},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 0),
                         createResolveInfo(PACKAGE2, getAppId(2), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false));
-        assertArrayEquals(new int[] {-1,-1,-1},
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 10),
                         createResolveInfo(PACKAGE2, getAppId(2), 10),
-                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false));
+                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false, mPlatformCompat));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
     public void testIsPrioritized_Yes() {
         assertTrue(isPrioritized(List.of(
@@ -151,18 +169,203 @@
                 createResolveInfo(PACKAGE2, getAppId(2), 0),
                 createResolveInfo(PACKAGE3, getAppId(3), 0))));
 
-        assertArrayEquals(new int[] {0,1,2},
+        assertArrayEquals(new int[] {0, 1, 2},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 10),
                         createResolveInfo(PACKAGE2, getAppId(2), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false));
-        assertArrayEquals(new int[] {0,0,2,3,3},
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 3, 3},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 20),
                         createResolveInfo(PACKAGE2, getAppId(2), 20),
                         createResolveInfo(PACKAGE3, getAppId(3), 10),
                         createResolveInfo(PACKAGE3, getAppId(3), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false));
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities() {
+        assertFalse(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertFalse(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 0, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 3, 3},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1, 0},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 2, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 2, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1, 1, 3},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(3), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(2), 0)), false, mPlatformCompat));
     }
 
     @Test
@@ -602,6 +805,66 @@
         assertTrue(record3.matchesDeliveryGroup(record1));
     }
 
+    @Test
+    public void testCalculateChangeStateForReceivers() {
+        assertArrayEquals(new boolean[] {true, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {true, true, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+        assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, true, false, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE2, getAppId(1)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+        assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, true, false, false, false, true},
+                calculateChangeState(
+                        List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                                createResolveInfo(PACKAGE3, getAppId(3)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE2, getAppId(1)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+        assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, false, false, false, false, false},
+                calculateChangeState(
+                        List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                                createResolveInfo(PACKAGE3, getAppId(3)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE2, getAppId(1)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE3, getAppId(3)))));
+    }
+
+    private boolean[] calculateChangeState(List<Object> receivers) {
+        return BroadcastRecord.calculateChangeStateForReceivers(receivers,
+                CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat);
+    }
+
     private static void cleanupDisabledPackageReceivers(BroadcastRecord record,
             String packageName, int userId) {
         record.cleanupDisabledPackageReceiversLocked(packageName, null /* filterByClasses */,
@@ -753,16 +1016,17 @@
                 BackgroundStartPrivileges.NONE,
                 false /* timeoutExempt */,
                 filterExtrasForReceiver,
-                PROCESS_STATE_UNKNOWN);
+                PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private static int getAppId(int i) {
         return Process.FIRST_APPLICATION_UID + i;
     }
 
-    private static boolean isPrioritized(List<Object> receivers) {
+    private boolean isPrioritized(List<Object> receivers) {
         return BroadcastRecord.isPrioritized(
-                calculateBlockedUntilBeyondCount(receivers, false), false);
+                calculateBlockedUntilBeyondCount(receivers, false, mPlatformCompat), false);
     }
 
     private static void assertBlocked(BroadcastRecord r, boolean... blocked) {
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index c645c08..9b7bbe0 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -114,6 +114,7 @@
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
+    <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" />
 
     <queries>
         <package android:name="com.android.servicestests.apps.suspendtestapp" />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 2edde9b..d5b9307 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -33,6 +33,7 @@
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE;
@@ -80,6 +81,7 @@
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Icon;
 import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.input.KeyGestureEvent;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -2183,6 +2185,168 @@
         verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
     }
 
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_toggleMagnifier() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction(
+                        KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).containsExactly(MAGNIFICATION_CONTROLLER_NAME);
+
+        // The magnifier will only be toggled on the second event received since the first is
+        // used to toggle the feature on.
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        verify(mInputFilter).notifyMagnificationShortcutTriggered(anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_trustedService() {
+        setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON);
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo trustedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(trustedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                trustedService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{trustedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).containsExactly(
+                trustedService.getComponentName().flattenToString());
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_preinstalledService() {
+        setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON);
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo untrustedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(untrustedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                untrustedService.getComponentName().flattenToString());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_downloadedService() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo downloadedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ false, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(downloadedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                downloadedService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{downloadedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_defaultNotInstalled() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        final AccessibilityServiceInfo defaultService = mockAccessibilityServiceInfo(
+                new ComponentName("package_b", "class_b"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(installedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                defaultService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{defaultService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_noDefault() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(installedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{installedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
 
     private Set<String> readStringsFromSetting(String setting) {
         final Set<String> result = new ArraySet<>();
@@ -2298,6 +2462,10 @@
                 AccessibilityManagerService service) {
             super(context, service);
         }
+
+        @Override
+        void notifyMagnificationShortcutTriggered(int displayId) {
+        }
     }
 
     private static class A11yTestableContext extends TestableContext {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index 8c35925..cb52eef 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -32,6 +32,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -174,6 +175,7 @@
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), SOFTWARE);
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), GESTURE);
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), QUICK_SETTINGS);
+        mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), KEY_GESTURE);
         mUserState.updateA11yTilesInQsPanelLocked(
                 Set.of(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME));
         mUserState.setTargetAssignedToAccessibilityButton(componentNameString);
@@ -201,6 +203,7 @@
         assertTrue(mUserState.getShortcutTargetsLocked(SOFTWARE).isEmpty());
         assertTrue(mUserState.getShortcutTargetsLocked(GESTURE).isEmpty());
         assertTrue(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS).isEmpty());
+        assertTrue(mUserState.getShortcutTargetsLocked(KEY_GESTURE).isEmpty());
         assertTrue(mUserState.getA11yQsTilesInQsPanel().isEmpty());
         assertNull(mUserState.getTargetAssignedToAccessibilityButton());
         assertFalse(mUserState.isTouchExplorationEnabledLocked());
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index 8164ef9..f0d3456 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -19,9 +19,13 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
 
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MAX_VALUE;
+import static com.android.internal.accessibility.common.MagnificationConstants.SCALE_MIN_VALUE;
 import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID;
 import static com.android.server.wm.WindowManagerInternal.AccessibilityControllerInternal.UiChangesForAccessibilityCallbacks;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -660,6 +664,90 @@
     }
 
     @Test
+    public void scaleMagnificationByStep_fullscreenMode_stepInAndOut() throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, 1.0f, false);
+        reset(mScreenMagnificationController);
+
+        // Verify the zoom scale factor increases by
+        // {@code MagnificationController.DefaultMagnificationScaleStepProvider
+        // .ZOOM_STEP_SCALE_FACTOR} and the center coordinates are
+        // unchanged (Float.NaN as values denotes unchanged center).
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_IN);
+        verify(mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY),
+                eq(MagnificationController
+                        .DefaultMagnificationScaleStepProvider.ZOOM_STEP_SCALE_FACTOR),
+                eq(Float.NaN), eq(Float.NaN), anyBoolean(), anyInt());
+
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_OUT);
+        verify(mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY),
+                eq(SCALE_MIN_VALUE), eq(Float.NaN), eq(Float.NaN), anyBoolean(), anyInt());
+    }
+
+    @Test
+    public void scaleMagnificationByStep_testMaxScaling() throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MIN_VALUE, false);
+        reset(mScreenMagnificationController);
+
+        float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+        while (currentScale < SCALE_MAX_VALUE) {
+            assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.ZOOM_DIRECTION_IN)).isTrue();
+            final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+            assertThat(nextScale).isGreaterThan(currentScale);
+            currentScale = nextScale;
+        }
+
+        assertThat(currentScale).isEqualTo(SCALE_MAX_VALUE);
+        assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_IN)).isFalse();
+    }
+
+    @Test
+    public void scaleMagnificationByStep_testMinScaling() throws RemoteException {
+        setMagnificationEnabled(MODE_FULLSCREEN);
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MAX_VALUE, false);
+        reset(mScreenMagnificationController);
+
+        float currentScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+        while (currentScale > SCALE_MIN_VALUE) {
+            assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                    MagnificationController.ZOOM_DIRECTION_OUT)).isTrue();
+            final float nextScale = mScreenMagnificationController.getScale(TEST_DISPLAY);
+            assertThat(nextScale).isLessThan(currentScale);
+            currentScale = nextScale;
+        }
+
+        assertThat(currentScale).isEqualTo(SCALE_MIN_VALUE);
+        assertThat(mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_OUT)).isFalse();
+    }
+
+    @Test
+    public void scaleMagnificationByStep_windowedMode_stepInAndOut() throws RemoteException {
+        setMagnificationEnabled(MODE_WINDOW);
+        mMagnificationController.onPerformScaleAction(TEST_DISPLAY, SCALE_MIN_VALUE, false);
+        reset(mMagnificationConnectionManager);
+
+        // Verify the zoom scale factor increases by
+        // {@code MagnificationController.DefaultMagnificationScaleStepProvider
+        // .ZOOM_STEP_SCALE_FACTOR}.
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_IN);
+        verify(mMagnificationConnectionManager).setScale(eq(TEST_DISPLAY),
+                eq(MagnificationController
+                        .DefaultMagnificationScaleStepProvider.ZOOM_STEP_SCALE_FACTOR));
+
+        mMagnificationController.scaleMagnificationByStep(TEST_DISPLAY,
+                MagnificationController.ZOOM_DIRECTION_OUT);
+        verify(mMagnificationConnectionManager).setScale(eq(TEST_DISPLAY),
+                eq(SCALE_MIN_VALUE));
+    }
+
+    @Test
     public void enableWindowMode_notifyMagnificationChanged() throws RemoteException {
         setMagnificationEnabled(MODE_WINDOW);
 
diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS b/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS
deleted file mode 100644
index 0218a78..0000000
--- a/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index bc410d9..88829c1 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -62,6 +62,7 @@
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.Flags;
 import android.hardware.biometrics.IBiometricAuthenticator;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
@@ -70,8 +71,16 @@
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IBiometricSysuiReceiver;
 import android.hardware.biometrics.PromptInfo;
+import android.hardware.biometrics.SensorProperties;
 import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.face.FaceManager;
+import android.hardware.face.FaceSensorProperties;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
 import android.hardware.keymaster.HardwareAuthenticatorType;
 import android.os.Binder;
 import android.os.Handler;
@@ -84,6 +93,7 @@
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
 import android.security.GateKeeper;
 import android.security.KeyStoreAuthorization;
 import android.service.gatekeeper.IGateKeeperService;
@@ -93,6 +103,7 @@
 import android.view.DisplayInfo;
 import android.view.WindowManager;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
@@ -111,6 +122,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
 
@@ -144,6 +156,27 @@
 
     private static final int SENSOR_ID_FINGERPRINT = 0;
     private static final int SENSOR_ID_FACE = 1;
+    private final ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback.Stub>
+            mFingerprintAuthenticatorRegisteredCallbackCaptor = ArgumentCaptor.forClass(
+            IFingerprintAuthenticatorsRegisteredCallback.Stub.class);
+    private final ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback.Stub>
+            mFaceAuthenticatorRegisteredCallbackCaptor = ArgumentCaptor.forClass(
+            IFaceAuthenticatorsRegisteredCallback.Stub.class);
+    private final ArgumentCaptor<BiometricStateListener> mBiometricStateListenerArgumentCaptor =
+            ArgumentCaptor.forClass(BiometricStateListener.class);
+    private final List<FingerprintSensorPropertiesInternal>
+            mFingerprintSensorPropertiesInternals = List.of(
+                    new FingerprintSensorPropertiesInternal(SENSOR_ID_FINGERPRINT,
+                            SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
+                            List.of(), FingerprintSensorProperties.TYPE_UNKNOWN,
+                            true /* resetLockoutRequiresHardwareAuthToken */));
+    private final List<FaceSensorPropertiesInternal>
+            mFaceSensorPropertiesInternals = List.of(
+                    new FaceSensorPropertiesInternal(SENSOR_ID_FACE,
+                            SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
+                            List.of(), FaceSensorProperties.TYPE_UNKNOWN,
+                            false /* supportsFaceDetection */, false /* supportsSelfIllumination */,
+                            false /* resetLockoutRequiresChallenge */));
 
     private BiometricService mBiometricService;
 
@@ -192,6 +225,10 @@
 
     @Mock
     private BiometricNotificationLogger mNotificationLogger;
+    @Mock
+    private FingerprintManager mFingerprintManager;
+    @Mock
+    private FaceManager mFaceManager;
 
     BiometricContextProvider mBiometricContextProvider;
 
@@ -1975,6 +2012,59 @@
                 eq(hardwareAuthenticators));
     }
 
+    @Test
+    public void testMandatoryBiometricsValue_whenParentProfileEnabled() throws RemoteException {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final int profileParentId = context.getContentResolver().getUserId();
+        final int userId = profileParentId + 1;
+        final BiometricService.SettingObserver settingObserver =
+                new BiometricService.SettingObserver(
+                        context, mBiometricHandlerProvider.getBiometricCallbackHandler(),
+                        new ArrayList<>(), mUserManager, mFingerprintManager, mFaceManager);
+
+        verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
+                mFingerprintAuthenticatorRegisteredCallbackCaptor.capture());
+        verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+                mFaceAuthenticatorRegisteredCallbackCaptor.capture());
+
+        mFingerprintAuthenticatorRegisteredCallbackCaptor.getValue().onAllAuthenticatorsRegistered(
+                mFingerprintSensorPropertiesInternals);
+        mFaceAuthenticatorRegisteredCallbackCaptor.getValue().onAllAuthenticatorsRegistered(
+                mFaceSensorPropertiesInternals);
+
+        verify(mFingerprintManager).registerBiometricStateListener(
+                mBiometricStateListenerArgumentCaptor.capture());
+
+        mBiometricStateListenerArgumentCaptor.getValue().onEnrollmentsChanged(userId,
+                SENSOR_ID_FINGERPRINT, true /* hasEnrollments */);
+
+        verify(mFaceManager).registerBiometricStateListener(
+                mBiometricStateListenerArgumentCaptor.capture());
+
+        mBiometricStateListenerArgumentCaptor.getValue().onEnrollmentsChanged(userId,
+                SENSOR_ID_FACE, true /* hasEnrollments */);
+
+        when(mUserManager.getProfileParent(userId)).thenReturn(new UserInfo(profileParentId,
+                "", 0));
+        when(mUserManager.getEnabledProfileIds(profileParentId)).thenReturn(new int[]{userId});
+
+        //Disable Identity Check for profile user
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.MANDATORY_BIOMETRICS, 0, userId);
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, 0,
+                userId);
+        //Enable Identity Check for parent user
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.MANDATORY_BIOMETRICS, 1, profileParentId);
+        Settings.Secure.putIntForUser(context.getContentResolver(),
+                Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, 1,
+                profileParentId);
+
+        assertTrue(settingObserver.getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(
+                userId));
+    }
+
     // Helper methods
 
     private int invokeCanAuthenticate(BiometricService service, int authenticators)
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
index 685e8d6..e611867 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
@@ -65,7 +65,7 @@
                 new Pair<>(Arrays.asList(mMockContextHubInfo), Arrays.asList(""));
         when(mMockContextHubInfo.getId()).thenReturn(CONTEXT_HUB_ID);
         when(mMockContextHubInfo.toString()).thenReturn(CONTEXT_HUB_STRING);
-        when(mMockContextHubWrapper.getHubs()).thenReturn(hubInfo);
+        when(mMockContextHubWrapper.getContextHubs()).thenReturn(hubInfo);
 
         when(mMockContextHubWrapper.supportsLocationSettingNotifications()).thenReturn(true);
         when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(true);
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index dddab65..5a7027e 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -2158,13 +2158,11 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testBackgroundChainEnabled() throws Exception {
         verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true);
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     @RequiresFlagsDisabled(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN)
     public void testBackgroundChainOnProcStateChangeSameDelay() throws Exception {
         // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
@@ -2194,10 +2192,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({
-            Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE,
-            Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN
-    })
+    @RequiresFlagsEnabled(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN)
     public void testBackgroundChainOnProcStateChangeDifferentDelays() throws Exception {
         // The app will be blocked when there is no prior proc-state.
         assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
@@ -2247,7 +2242,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testBackgroundChainOnAllowlistChange() throws Exception {
         // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
         clearInvocations(mNetworkManager);
@@ -2285,7 +2279,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testBackgroundChainOnTempAllowlistChange() throws Exception {
         // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
         clearInvocations(mNetworkManager);
@@ -2387,7 +2380,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testUidObserverFiltersProcStateChanges() throws Exception {
         int testProcStateSeq = 0;
         try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) {
@@ -2450,7 +2442,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testUidObserverFiltersStaleChanges() throws Exception {
         final int testProcStateSeq = 51;
         try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) {
@@ -2470,7 +2461,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testUidObserverFiltersCapabilityChanges() throws Exception {
         int testProcStateSeq = 0;
         try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) {
@@ -2559,7 +2549,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
     public void testObsoleteHandleUidChanged() throws Exception {
         callAndWaitOnUidGone(UID_A);
         assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationServiceTest.java
similarity index 94%
rename from services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java
rename to services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationServiceTest.java
index d180688..154494a 100644
--- a/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/AdaptiveAuthenticationServiceTest.java
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.adaptiveauth;
+package com.android.server.security.adaptiveauthentication;
 
 import static android.adaptiveauth.Flags.FLAG_ENABLE_ADAPTIVE_AUTH;
 import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
 import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
-import static com.android.server.adaptiveauth.AdaptiveAuthService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS;
+import static com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
@@ -66,12 +66,12 @@
 import org.mockito.MockitoAnnotations;
 
 /**
- * atest FrameworksServicesTests:AdaptiveAuthServiceTest
+ * atest FrameworksServicesTests:AdaptiveAuthenticationServiceTest
  */
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class AdaptiveAuthServiceTest {
+public class AdaptiveAuthenticationServiceTest {
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
@@ -81,7 +81,7 @@
     private static final int REASON_UNKNOWN = 0; // BiometricRequestConstants.RequestReason
 
     private Context mContext;
-    private AdaptiveAuthService mAdaptiveAuthService;
+    private AdaptiveAuthenticationService mAdaptiveAuthenticationService;
 
     @Mock
     LockPatternUtils mLockPatternUtils;
@@ -124,8 +124,9 @@
         LocalServices.removeServiceForTest(UserManagerInternal.class);
         LocalServices.addService(UserManagerInternal.class, mUserManager);
 
-        mAdaptiveAuthService = new AdaptiveAuthService(mContext, mLockPatternUtils);
-        mAdaptiveAuthService.init();
+        mAdaptiveAuthenticationService = new AdaptiveAuthenticationService(
+                mContext, mLockPatternUtils);
+        mAdaptiveAuthenticationService.init();
 
         verify(mLockSettings).registerLockSettingsStateListener(
                 mLockSettingsStateListenerCaptor.capture());
@@ -317,13 +318,13 @@
 
     private void verifyNotLockDevice(int expectedCntFailedAttempts, int userId) {
         assertEquals(expectedCntFailedAttempts,
-                mAdaptiveAuthService.mFailedAttemptsForUser.get(userId));
+                mAdaptiveAuthenticationService.mFailedAttemptsForUser.get(userId));
         verify(mWindowManager, never()).lockNow();
     }
 
     private void verifyLockDevice(int userId) {
         assertEquals(MAX_ALLOWED_FAILED_AUTH_ATTEMPTS,
-                mAdaptiveAuthService.mFailedAttemptsForUser.get(userId));
+                mAdaptiveAuthenticationService.mFailedAttemptsForUser.get(userId));
         verify(mLockPatternUtils).requireStrongAuth(
                 eq(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST), eq(userId));
         // If userId is MANAGED_PROFILE_USER_ID, the StrongAuthFlag of its parent (PRIMARY_USER_ID)
diff --git a/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS
new file mode 100644
index 0000000..bc8efa9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/security/adaptiveauthentication/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/security/adaptiveauthentication/OWNERS
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
index c9d5241..b3ec215 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java
@@ -30,7 +30,6 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.server.pm.UserManagerInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
 
 import org.junit.After;
@@ -42,7 +41,6 @@
 
 public class UiServiceTestCase {
     @Mock protected PackageManagerInternal mPmi;
-    @Mock protected UserManagerInternal mUmi;
     @Mock protected UriGrantsManagerInternal mUgmInternal;
 
     protected static final String PKG_N_MR1 = "com.example.n_mr1";
@@ -94,8 +92,6 @@
                     }
                 });
 
-        LocalServices.removeServiceForTest(UserManagerInternal.class);
-        LocalServices.addService(UserManagerInternal.class, mUmi);
         LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
         LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
         when(mUgmInternal.checkGrantUriPermission(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 0b89c11..cc02865 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2291,7 +2291,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_CLASSIFICATION,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
     public void testMoveAggregateGroups_updateChannel_multipleChannels_regroupOnClassifEnabled() {
         final String pkg = "package";
         final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
@@ -2366,7 +2368,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_CLASSIFICATION,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
     public void testMoveSections_notificationBundled() {
         final List<NotificationRecord> notificationList = new ArrayList<>();
         final String pkg = "package";
@@ -2406,11 +2410,13 @@
                 NotificationChannel.NEWS_ID);
         for (NotificationRecord record: notificationList) {
             if (record.getChannel().getId().equals(channel1.getId())
+                    && record.getNotification().isGroupChild()
                     && record.getSbn().getId() % 2 == 0) {
                 record.updateNotificationChannel(socialChannel);
                 mGroupHelper.onChannelUpdated(record);
             }
             if (record.getChannel().getId().equals(channel1.getId())
+                    && record.getNotification().isGroupChild()
                     && record.getSbn().getId() % 2 != 0) {
                 record.updateNotificationChannel(newsChannel);
                 mGroupHelper.onChannelUpdated(record);
@@ -2436,7 +2442,9 @@
     }
 
     @Test
-    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_CLASSIFICATION,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
     public void testCacheAndCancelAppSummary_notificationBundled() {
         // check that the original app summary is canceled & cached on classification regrouping
         final List<NotificationRecord> notificationList = new ArrayList<>();
@@ -2468,7 +2476,8 @@
                 NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
                 IMPORTANCE_DEFAULT);
         for (NotificationRecord record: notificationList) {
-            if (record.getChannel().getId().equals(channel1.getId())) {
+            if (record.getChannel().getId().equals(channel1.getId())
+                    && record.getNotification().isGroupChild()) {
                 record.updateNotificationChannel(socialChannel);
                 mGroupHelper.onChannelUpdated(record);
             }
@@ -2495,6 +2504,7 @@
 
     @Test
     @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_CLASSIFICATION,
             FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
             FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
     public void testSingletonGroupsRegrouped_notificationBundledBeforeDelayTimeout() {
@@ -2525,7 +2535,8 @@
                 BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
                 NotificationChannel.SOCIAL_MEDIA_ID);
         for (NotificationRecord record: notificationList) {
-            if (record.getOriginalGroupKey().contains("testGrp")) {
+            if (record.getOriginalGroupKey().contains("testGrp")
+                    && record.getNotification().isGroupChild()) {
                 record.updateNotificationChannel(socialChannel);
                 mGroupHelper.onChannelUpdated(record);
             }
@@ -2569,6 +2580,7 @@
 
     @Test
     @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_CLASSIFICATION,
             FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
             FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
     public void testSingletonGroupsRegrouped_notificationBundledAfterDelayTimeout() {
@@ -2623,7 +2635,8 @@
                 BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
                 NotificationChannel.SOCIAL_MEDIA_ID);
         for (NotificationRecord record: notificationList) {
-            if (record.getOriginalGroupKey().contains("testGrp")) {
+            if (record.getOriginalGroupKey().contains("testGrp")
+                    && record.getNotification().isGroupChild()) {
                 record.updateNotificationChannel(socialChannel);
                 mGroupHelper.onChannelUpdated(record);
             }
@@ -2642,6 +2655,64 @@
     }
 
     @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
+            FLAG_NOTIFICATION_CLASSIFICATION})
+    public void testValidGroupsRegrouped_notificationBundledWhileEnqueued() {
+        // Check that valid group notifications are regrouped if classification is done
+        // before onNotificationPostedWithDelay (within DELAY_FOR_ASSISTANT_TIME)
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+        final String pkg = "package";
+
+        final int summaryId = 0;
+        final int numChildren = 3;
+        // Post a regular/valid group: summary + notifications
+        NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+                String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+        notificationList.add(summary);
+        summaryByGroup.put(summary.getGroupKey(), summary);
+        for (int i = 0; i < numChildren; i++) {
+            NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+                    UserHandle.SYSTEM, "testGrp", false);
+            notificationList.add(child);
+        }
+
+        // Classify/bundle child notifications. Don't call onChannelUpdated,
+        // adjustments applied while enqueued will use NotificationAdjustmentExtractor.
+        final NotificationChannel socialChannel = new NotificationChannel(
+                NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+                IMPORTANCE_DEFAULT);
+        final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
+        final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
+                BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                NotificationChannel.SOCIAL_MEDIA_ID);
+        for (NotificationRecord record: notificationList) {
+            if (record.getOriginalGroupKey().contains("testGrp")
+                    && record.getNotification().isGroupChild()) {
+                record.updateNotificationChannel(socialChannel);
+            }
+        }
+
+        // Check that notifications are forced grouped and app-provided summaries are canceled
+        for (NotificationRecord record: notificationList) {
+            mGroupHelper.onNotificationPostedWithDelay(record, notificationList, summaryByGroup);
+        }
+
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
+        verify(mCallback, times(numChildren)).addAutoGroup(anyString(), eq(expectedGroupKey_social),
+                eq(true));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, times(numChildren - 1)).updateAutogroupSummary(anyInt(), anyString(),
+                anyString(), any());
+        verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification(anyString(),
+                anyString());
+    }
+
+    @Test
     @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
     public void testMoveAggregateGroups_updateChannel_groupsUngrouped() {
         final String pkg = "package";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 48bc9d7..e5c42082 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -1484,7 +1484,6 @@
         assertTrue(componentsToUnbind.get(0).contains(ComponentName.unflattenFromString("c/c")));
     }
 
-    @SuppressWarnings("GuardedBy")
     @Test
     public void populateComponentsToBind() {
         ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
@@ -1508,8 +1507,7 @@
 
         SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
 
-        service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser,
-                /* isVisibleBackgroundUser= */ false);
+        service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser);
 
         assertEquals(2, componentsToBind.size());
         assertEquals(1, componentsToBind.get(0).size());
@@ -1519,33 +1517,6 @@
         assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c")));
     }
 
-    @SuppressWarnings("GuardedBy")
-    @Test
-    public void populateComponentsToBind_isVisibleBackgroundUser_addComponentsToBindButNotAddToEnabledComponent() {
-        ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
-                APPROVAL_BY_COMPONENT);
-
-        SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>();
-        ArraySet<ComponentName> allowed = new ArraySet<>();
-        allowed.add(ComponentName.unflattenFromString("pkg1/cmp1"));
-        approvedComponentsByUser.put(11, allowed);
-        IntArray users = new IntArray();
-        users.add(11);
-
-        SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>();
-
-        service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser,
-                /* isVisibleBackgroundUser= */ true);
-
-        assertEquals(1, componentsToBind.size());
-        assertEquals(1, componentsToBind.get(11).size());
-        assertTrue(componentsToBind.get(11).contains(ComponentName.unflattenFromString(
-                "pkg1/cmp1")));
-        assertThat(service.isComponentEnabledForCurrentProfiles(
-                new ComponentName("pkg1", "cmp1"))).isFalse();
-        assertThat(service.isComponentEnabledForPackage("pkg1")).isFalse();
-    }
-
     @Test
     public void testOnNullBinding() throws Exception {
         Context context = mock(Context.class);
diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml
index c0f514f..850884f 100644
--- a/services/tests/vibrator/AndroidManifest.xml
+++ b/services/tests/vibrator/AndroidManifest.xml
@@ -32,6 +32,9 @@
     <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" />
     <!-- Required to play system-only haptic feedback constants -->
     <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" />
+    <!-- Required to play vendor effects and start vendor sessions -->
+    <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" />
+    <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.mock" android:required="true" />
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index dfdd0cd..88ba9e3 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -83,6 +84,8 @@
 import android.os.VibratorInfo;
 import android.os.test.FakeVibrator;
 import android.os.test.TestLooper;
+import android.os.vibrator.IVibrationSession;
+import android.os.vibrator.IVibrationSessionCallback;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
 import android.os.vibrator.StepSegment;
@@ -195,6 +198,7 @@
             new SparseArray<>();
 
     private final List<HalVibration> mPendingVibrations = new ArrayList<>();
+    private final List<VendorVibrationSession> mPendingSessions = new ArrayList<>();
 
     private VibratorManagerService mService;
     private Context mContextSpy;
@@ -264,6 +268,11 @@
             grantPermission(android.Manifest.permission.VIBRATE);
             // Cancel any pending vibration from tests, including external vibrations.
             cancelVibrate(mService);
+            // End pending sessions.
+            for (VendorVibrationSession session : mPendingSessions) {
+                session.cancelSession();
+            }
+            mTestLooper.dispatchAll();
             // Wait until pending vibrations end asynchronously.
             for (HalVibration vibration : mPendingVibrations) {
                 vibration.waitForEnd();
@@ -1229,6 +1238,36 @@
                 .anyMatch(PrebakedSegment.class::isInstance));
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void vibrate_withOngoingHigherImportanceSession_ignoresEffect() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        HalVibration vibration = vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        assertThat(vibration.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE);
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+        // The second vibration shouldn't have played any prebaked segment.
+        assertFalse(fakeVibrator.getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
+    }
+
     @Test
     public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect()
             throws Exception {
@@ -1289,6 +1328,36 @@
                 .filter(PrebakedSegment.class::isInstance).count());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void vibrate_withOngoingLowerImportanceSession_cancelsOngoingSession() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        HalVibration vibration = vibrateAndWaitUntilFinished(service,
+                VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                HAPTIC_FEEDBACK_ATTRS);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        assertThat(vibration.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+        // One segment played is the prebaked CLICK from the new vibration.
+        assertEquals(1, mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .filter(PrebakedSegment.class::isInstance).count());
+    }
+
     @Test
     public void vibrate_withOngoingSameImportancePipelinedVibration_continuesOngoingEffect()
             throws Exception {
@@ -1416,16 +1485,16 @@
         // The native callback will be dispatched manually in this test.
         mTestLooper.stopAutoDispatchAndIgnoreExceptions();
 
-        ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor =
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
                 ArgumentCaptor.forClass(
-                        VibratorManagerService.OnSyncedVibrationCompleteListener.class);
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
         verify(mNativeWrapperMock).init(listenerCaptor.capture());
 
         CountDownLatch triggerCountDown = new CountDownLatch(1);
         // Mock trigger callback on registered listener right after the synced vibration starts.
         when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true);
         when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> {
-            listenerCaptor.getValue().onComplete(answer.getArgument(0));
+            listenerCaptor.getValue().onSyncedVibrationComplete(answer.getArgument(0));
             triggerCountDown.countDown();
             return true;
         });
@@ -2318,6 +2387,34 @@
         assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void onExternalVibration_withOngoingHigherImportanceSession_ignoreNewVibration()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class));
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        // External vibration is ignored.
+        assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+
+        // Session still running.
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+    }
+
     @Test
     public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration()
             throws Exception {
@@ -2373,6 +2470,36 @@
         assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    @Test
+    public void onExternalVibration_withOngoingLowerImportanceSession_cancelsOngoingSession()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class));
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+        mTestLooper.dispatchAll();
+
+        // Session is cancelled.
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+        assertEquals(Arrays.asList(false, true),
+                mVibratorProviders.get(1).getExternalControlStates());
+    }
+
     @Test
     public void onExternalVibration_withRingtone_usesRingerModeSettings() {
         mockVibrators(1);
@@ -2638,6 +2765,376 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutFeatureFlag_throwsException() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        assertThrows("Expected starting session without feature flag to fail!",
+                UnsupportedOperationException.class,
+                () -> startSession(service, RINGTONE_ATTRS, callback, vibratorId));
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onStarted(any(IVibrationSession.class));
+        verify(callback, never()).onFinishing();
+        verify(callback, never()).onFinished(anyInt());
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutCapability_doesNotStart() throws Exception {
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS,
+                callback, vibratorId);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onFinishing();
+        verify(callback)
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutCallback_doesNotStart() {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        int vibratorId = 1;
+        mockVibrators(vibratorId);
+        VibratorManagerService service = createSystemReadyService();
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS,
+                /* callback= */ null, vibratorId);
+        mTestLooper.dispatchAll();
+
+        assertThat(session).isNull();
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withoutVibratorIds_doesNotStart() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+
+        int[] nullIds = null;
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, nullIds);
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+
+        int[] emptyIds = {};
+        session = startSession(service, RINGTONE_ATTRS, callback, emptyIds);
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class));
+        verify(callback, never()).onFinishing();
+        verify(callback, times(2))
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_badVibratorId_failsToStart() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        when(mNativeWrapperMock.startSession(anyLong(), any(int[].class))).thenReturn(false);
+        doReturn(false).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 3}));
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 2}));
+        VibratorManagerService service = createSystemReadyService();
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 3);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED);
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 3}));
+        verify(callback, never()).onStarted(any(IVibrationSession.class));
+        verify(callback, never()).onFinishing();
+        verify(callback)
+                .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenFinish_returnsSuccessAfterCallback() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        int sessionFinishDelayMs = 200;
+        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().finishSession();
+
+        // Session not ended until HAL callback.
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+
+        // Dispatch HAL callbacks.
+        mTestLooper.moveTimeForward(sessionFinishDelayMs);
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenSendCancelSignal_cancelsSession() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        int sessionFinishDelayMs = 200;
+        IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        session.getCancellationSignal().cancel();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenCancel_returnsCancelStatus() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        // Delay not applied when session is aborted.
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_finishThenCancel_returnsRightAwayWithFinishedStatus()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        // Delay not applied when session is aborted.
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+
+        captor.getValue().finishSession();
+        mTestLooper.dispatchAll();
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.FINISHED);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_thenHalCancels_returnsCancelStatus()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1, 2);
+        VibratorManagerService service = createSystemReadyService();
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
+                ArgumentCaptor.forClass(
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
+        verify(mNativeWrapperMock).init(listenerCaptor.capture());
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class));
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2}));
+        verify(callback).onStarted(any(IVibrationSession.class));
+
+        // Mock HAL ending session unexpectedly.
+        listenerCaptor.getValue().onVibrationSessionComplete(session.getSessionId());
+        mTestLooper.dispatchAll();
+
+        assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_UNKNOWN_REASON);
+        verify(callback).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withPowerMode_usesPowerModeState() throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+        VendorVibrationSession session1 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        VendorVibrationSession session2 = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class);
+        verify(callback).onStarted(captor.capture());
+        captor.getValue().cancelSession();
+        mTestLooper.dispatchAll();
+
+        mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+        VendorVibrationSession session3 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never())
+                .startSession(eq(session1.getSessionId()), any(int[].class));
+        verify(mNativeWrapperMock).startSession(eq(session2.getSessionId()), eq(new int[] {1}));
+        verify(mNativeWrapperMock).startSession(eq(session3.getSessionId()), eq(new int[] {1}));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingHigherImportanceVibration_ignoresSession()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{128, 255}, -1);
+        vibrate(service, effect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async.
+        // Wait until second step started to ensure the noteVibratorOn was triggered.
+        assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2,
+                service, TEST_TIMEOUT_MILLIS));
+
+        VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        verify(mNativeWrapperMock, never())
+                .startSession(eq(session.getSessionId()), any(int[].class));
+        assertThat(session.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE);
+        verify(callback, never()).onFinishing();
+        verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_IGNORED));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingLowerImportanceVibration_cancelsOngoing()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{128, 255}, -1);
+        HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+
+        // VibrationThread will start this vibration async.
+        // Wait until second step started to ensure the noteVibratorOn was triggered.
+        assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, service,
+                TEST_TIMEOUT_MILLIS));
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        vibration.waitForEnd();
+        assertTrue(waitUntil(s -> session.isStarted(), service, TEST_TIMEOUT_MILLIS));
+        mTestLooper.dispatchAll();
+
+        assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED);
+        assertThat(session.getStatus()).isEqualTo(Status.RUNNING);
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 }));
+        verify(callback).onStarted(any(IVibrationSession.class));
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+    public void startVibrationSession_withOngoingLowerImportanceExternalVibration_cancelsOngoing()
+            throws Exception {
+        mockCapabilities(IVibratorManager.CAP_START_SESSIONS);
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        VibratorManagerService service = createSystemReadyService();
+        IVibrationSessionCallback callback =
+                mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS);
+
+        IBinder firstToken = mock(IBinder.class);
+        IExternalVibrationController controller = mock(IExternalVibrationController.class);
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                AUDIO_ALARM_ATTRS,
+                controller, firstToken);
+        ExternalVibrationScale scale =
+                mExternalVibratorService.onExternalVibrationStart(externalVibration);
+
+        VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1);
+        mTestLooper.dispatchAll();
+
+        assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel);
+        // The external vibration should have been cancelled
+        verify(controller).mute();
+        assertEquals(Arrays.asList(false, true, false),
+                mVibratorProviders.get(1).getExternalControlStates());
+        verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 }));
+        verify(callback).onStarted(any(IVibrationSession.class));
+    }
+
+    @Test
     public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
@@ -3050,6 +3547,30 @@
         when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds);
     }
 
+    private IVibrationSessionCallback mockSessionCallbacks(long delayToEndSessionMillis) {
+        Handler handler = new Handler(mTestLooper.getLooper());
+        ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor =
+                ArgumentCaptor.forClass(
+                        VibratorManagerService.VibratorManagerNativeCallbacks.class);
+        verify(mNativeWrapperMock).init(listenerCaptor.capture());
+        doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class));
+        doAnswer(args -> {
+            handler.postDelayed(
+                    () -> listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)),
+                    delayToEndSessionMillis);
+            return null;
+        }).when(mNativeWrapperMock).endSession(anyLong(), eq(false));
+        doAnswer(args -> {
+            listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0));
+            return null;
+        }).when(mNativeWrapperMock).endSession(anyLong(), eq(true));
+
+        IBinder token = mock(IBinder.class);
+        IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class);
+        doReturn(token).when(callback).asBinder();
+        return callback;
+    }
+
     private void cancelVibrate(VibratorManagerService service) {
         service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service);
     }
@@ -3157,6 +3678,16 @@
         return vib;
     }
 
+    private VendorVibrationSession startSession(VibratorManagerService service,
+            VibrationAttributes attrs, IVibrationSessionCallback callback, int... vibratorIds) {
+        VendorVibrationSession session = service.startVendorVibrationSessionInternal(UID,
+                Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, vibratorIds, attrs, "reason", callback);
+        if (session != null) {
+            mPendingSessions.add(session);
+        }
+        return session;
+    }
+
     private boolean waitUntil(Predicate<VibratorManagerService> predicate,
             VibratorManagerService service, long timeout) throws InterruptedException {
         long timeoutTimestamp = SystemClock.uptimeMillis() + timeout;
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 28ae271..cf5323e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -288,7 +288,6 @@
      * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
      */
     @Test
-    @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
     @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
     public void testTakeScreenshot_flagEnabled() {
         sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
@@ -296,17 +295,6 @@
     }
 
     /**
-     * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
-     */
-    @Test
-    @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE,
-            com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
-    public void testTakeScreenshot_flagDisabled() {
-        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
-        mPhoneWindowManager.assertTakeScreenshotNotCalled();
-    }
-
-    /**
      * META+CTRL+BACKSPACE for taking a bugreport when the flag is enabled.
      */
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 6596ee9..a51ce995 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -699,8 +699,8 @@
 
     void assertPowerWakeUp() {
         mTestLooper.dispatchAll();
-        verify(mWindowWakeUpPolicy)
-                .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
+        verify(mWindowWakeUpPolicy).wakeUpFromKey(
+                eq(DEFAULT_DISPLAY), anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
     }
 
     void assertNoPowerSleep() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
index 7322e5a..3ca352c 100644
--- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
@@ -22,6 +22,7 @@
 import static android.os.PowerManager.WAKE_REASON_POWER_BUTTON;
 import static android.os.PowerManager.WAKE_REASON_WAKE_KEY;
 import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.KeyEvent.KEYCODE_HOME;
@@ -35,6 +36,7 @@
 import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch;
 import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture;
 import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE;
+import static com.android.server.power.feature.flags.Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -43,6 +45,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -52,6 +55,8 @@
 import android.content.ContextWrapper;
 import android.content.res.Resources;
 import android.os.PowerManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.view.Display;
@@ -125,6 +130,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
         setTheaterModeEnabled(false);
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -136,7 +142,8 @@
 
         // Verify the policy wake up call succeeds because of the call on the delegate, and not
         // because of a PowerManager wake up.
-        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue();
+        assertThat(mPolicy.wakeUpFromMotion(
+                mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true);
         verifyNoPowerManagerWakeUp();
 
@@ -144,12 +151,14 @@
 
         // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
         // delegate would not handle the wake up request.
-        assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue();
+        assertThat(mPolicy.wakeUpFromMotion(
+                mDefaultDisplay.getDisplayId(), 300, SOURCE_ROTARY_ENCODER, false)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false);
         verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
         setTheaterModeEnabled(false);
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -161,7 +170,7 @@
 
         // Verify the policy wake up call succeeds because of the call on the delegate, and not
         // because of a PowerManager wake up.
-        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue();
+        assertThat(mPolicy.wakeUpFromKey(DEFAULT_DISPLAY, 200, KEYCODE_POWER, true)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true);
         verifyNoPowerManagerWakeUp();
 
@@ -169,7 +178,8 @@
 
         // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
         // delegate would not handle the wake up request.
-        assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue();
+        assertThat(mPolicy.wakeUpFromKey(
+                DEFAULT_DISPLAY, 300, KEYCODE_STEM_PRIMARY, false)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false);
         verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY");
     }
@@ -186,7 +196,7 @@
                 .setInputWakeUpDelegate(mInputWakeUpDelegate);
 
         // Check that the wake up does not happen because the theater mode policy check fails.
-        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse();
+        assertThat(mPolicy.wakeUpFromKey(DEFAULT_DISPLAY, 200, KEYCODE_POWER, true)).isFalse();
         verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean());
     }
 
@@ -201,11 +211,13 @@
                 .setInputWakeUpDelegate(mInputWakeUpDelegate);
 
         // Check that the wake up does not happen because the theater mode policy check fails.
-        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse();
+        assertThat(mPolicy.wakeUpFromMotion(
+                mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isFalse();
         verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean());
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testTheaterModeChecksNotAppliedWhenScreenIsOn() {
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
         setDefaultDisplayState(Display.STATE_ON);
@@ -213,30 +225,69 @@
         setBooleanRes(config_allowTheaterModeWakeFromMotion, false);
         mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
 
-        mPolicy.wakeUpFromMotion(200L, SOURCE_TOUCHSCREEN, true);
+        mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(), 200L, SOURCE_TOUCHSCREEN, true);
 
         verify(mPowerManager).wakeUp(200L, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromMotion() {
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
+                () -> mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(),
+                        mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
                 config_allowTheaterModeWakeFromMotion,
                 WAKE_REASON_WAKE_MOTION,
                 "android.policy:MOTION");
     }
 
     @Test
+    @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
+    public void testWakeUpFromMotion_perDisplayWakeByTouchEnabled() {
+        setTheaterModeEnabled(false);
+        final int displayId = 555;
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+
+        boolean displayWokeUp = mPolicy.wakeUpFromMotion(
+                displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
+
+        // Verify that display is woken up
+        assertThat(displayWokeUp).isTrue();
+        verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
+                eq("android.policy:MOTION"), eq(displayId));
+    }
+
+    @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
+    public void testWakeUpFromMotion_perDisplayWakeByTouchDisabled() {
+        setTheaterModeEnabled(false);
+        final int displayId = 555;
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+
+        boolean displayWokeUp = mPolicy.wakeUpFromMotion(
+                displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
+
+        // Verify that power is woken up and display isn't woken up individually
+        assertThat(displayWokeUp).isTrue();
+        verify(mPowerManager).wakeUp(
+                anyLong(), eq(WAKE_REASON_WAKE_MOTION), eq("android.policy:MOTION"));
+        verify(mPowerManager, never()).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
+                eq("android.policy:MOTION"), eq(displayId));
+    }
+
+    @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromKey_nonPowerKey() {
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true),
+                () -> mPolicy.wakeUpFromKey(
+                        DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_HOME, true),
                 config_allowTheaterModeWakeFromKey,
                 WAKE_REASON_WAKE_KEY,
                 "android.policy:KEY");
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromKey_powerKey() {
         // Disable the resource affecting all wake keys because it affects power key as well.
         // That way, power key wake during theater mode will solely be controlled by
@@ -245,7 +296,8 @@
 
         // Test with power key
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true),
+                () -> mPolicy.wakeUpFromKey(
+                        DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_POWER, true),
                 config_allowTheaterModeWakeFromPowerKey,
                 WAKE_REASON_POWER_BUTTON,
                 "android.policy:POWER");
@@ -254,13 +306,31 @@
         // even if the power-key specific theater mode config is disabled.
         setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false);
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false),
+                () -> mPolicy.wakeUpFromKey(
+                        DEFAULT_DISPLAY, mClock.uptimeMillis(), KEYCODE_POWER, false),
                 config_allowTheaterModeWakeFromKey,
                 WAKE_REASON_POWER_BUTTON,
                 "android.policy:POWER");
     }
 
     @Test
+    @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
+    public void testWakeUpFromKey_invalidDisplay_perDisplayWakeByTouchEnabled() {
+        setTheaterModeEnabled(false);
+        final int displayId = Display.INVALID_DISPLAY;
+        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
+
+        boolean displayWokeUp = mPolicy.wakeUpFromKey(
+                displayId, mClock.uptimeMillis(), KEYCODE_POWER, /* isDown= */ false);
+
+        // Verify that default display is woken up
+        assertThat(displayWokeUp).isTrue();
+        verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_POWER_BUTTON),
+                eq("android.policy:POWER"), eq(DEFAULT_DISPLAY));
+    }
+
+    @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromLid() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromLid(),
@@ -270,6 +340,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromWakeGesture() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromWakeGesture(),
@@ -279,6 +350,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testwakeUpFromCameraCover() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()),
@@ -288,6 +360,7 @@
     }
 
     @Test
+    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromPowerKeyCameraGesture() {
         // Disable the resource affecting all wake keys because it affects power key as well.
         // That way, power key wake during theater mode will solely be controlled by
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index 9967cce..7dba142 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -21,7 +21,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
 import static org.junit.Assert.assertEquals;
@@ -165,31 +164,6 @@
     }
 
     @Test
-    public void testDelayingAnimationStart() {
-        mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
-        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        verifyZeroInteractions(mSpec);
-        assertAnimating(mAnimatable);
-        assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed());
-        mAnimatable.mSurfaceAnimator.endDelayingAnimationStart();
-        verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), any());
-    }
-
-    @Test
-    public void testDelayingAnimationStartAndCancelled() {
-        mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
-        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        mAnimatable.mSurfaceAnimator.cancelAnimation();
-        verifyZeroInteractions(mSpec);
-        assertNotAnimating(mAnimatable);
-        assertTrue(mAnimatable.mFinishedCallbackCalled);
-        assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType);
-        verify(mTransaction).remove(eq(mAnimatable.mLeash));
-    }
-
-    @Test
     public void testTransferAnimation() {
         mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
                 ANIMATION_TYPE_APP_TRANSITION);
diff --git a/telecomm/java/android/telecom/Log.java b/telecomm/java/android/telecom/Log.java
index a34094c..98949d0c 100644
--- a/telecomm/java/android/telecom/Log.java
+++ b/telecomm/java/android/telecom/Log.java
@@ -68,7 +68,7 @@
     // Used to synchronize singleton logging lazy initialization
     private static final Object sSingletonSync = new Object();
     private static EventManager sEventManager;
-    private static SessionManager sSessionManager;
+    private static volatile SessionManager sSessionManager;
     private static Object sLock = null;
 
     /**
@@ -379,6 +379,23 @@
         return sSessionManager;
     }
 
+    @VisibleForTesting
+    public static SessionManager setSessionManager(Context context,
+            java.lang.Runnable cleanSessionRunnable) {
+        // Checking for null again outside of synchronization because we only need to synchronize
+        // during the lazy loading of the session logger. We don't need to synchronize elsewhere.
+        if (sSessionManager == null) {
+            synchronized (sSingletonSync) {
+                if (sSessionManager == null) {
+                    sSessionManager = new SessionManager(cleanSessionRunnable);
+                    sSessionManager.setContext(context);
+                    return sSessionManager;
+                }
+            }
+        }
+        return sSessionManager;
+    }
+
     public static void setTag(String tag) {
         TAG = tag;
         DEBUG = isLoggable(android.util.Log.DEBUG);
diff --git a/telecomm/java/android/telecom/Logging/SessionManager.java b/telecomm/java/android/telecom/Logging/SessionManager.java
index 00e344c..ac1e69e 100644
--- a/telecomm/java/android/telecom/Logging/SessionManager.java
+++ b/telecomm/java/android/telecom/Logging/SessionManager.java
@@ -62,9 +62,7 @@
 
     @VisibleForTesting
     public final ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(64);
-    @VisibleForTesting
-    public java.lang.Runnable mCleanStaleSessions = () ->
-            cleanupStaleSessions(getSessionCleanupTimeoutMs());
+    private final java.lang.Runnable mCleanStaleSessions;
     private final Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
 
     // Overridden in LogTest to skip query to ContentProvider
@@ -110,29 +108,39 @@
     }
 
     public SessionManager() {
+        mCleanStaleSessions = () -> cleanupStaleSessions(getSessionCleanupTimeoutMs());
+    }
+
+    @VisibleForTesting
+    public SessionManager(java.lang.Runnable cleanStaleSessionsRunnable) {
+        mCleanStaleSessions = cleanStaleSessionsRunnable;
     }
 
     private long getSessionCleanupTimeoutMs() {
         return mSessionCleanupTimeoutMs.get();
     }
 
-    private synchronized void resetStaleSessionTimer() {
+    private void resetStaleSessionTimer() {
         if (!Flags.endSessionImprovements()) {
-            mSessionCleanupHandler.removeCallbacksAndMessages(null);
-            // Will be null in Log Testing
-            if (mCleanStaleSessions != null) {
-                mSessionCleanupHandler.postDelayed(mCleanStaleSessions,
-                        getSessionCleanupTimeoutMs());
-            }
-        } else {
-            if (mCleanStaleSessions != null
-                    && !mSessionCleanupHandler.hasCallbacks(mCleanStaleSessions)) {
+            resetStaleSessionTimerOld();
+            return;
+        }
+        // Will be null in Log Testing
+        if (mCleanStaleSessions == null) return;
+        synchronized (mSessionCleanupHandler) {
+            if (!mSessionCleanupHandler.hasCallbacks(mCleanStaleSessions)) {
                 mSessionCleanupHandler.postDelayed(mCleanStaleSessions,
                         getSessionCleanupTimeoutMs());
             }
         }
     }
 
+    private synchronized void resetStaleSessionTimerOld() {
+        if (mCleanStaleSessions == null) return;
+        mSessionCleanupHandler.removeCallbacksAndMessages(null);
+        mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs());
+    }
+
     /**
      * Determines whether or not to start a new session or continue an existing session based on
      * the {@link Session.Info} info passed into startSession. If info is null, a new Session is
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 2a06c3d..6490cbe 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -2183,8 +2183,8 @@
      * Maximum size in bytes of the PDU to send or download when connected to a non-terrestrial
      * network. MmsService will return a result code of MMS_ERROR_TOO_LARGE_FOR_TRANSPORT if
      * the PDU exceeds this limit when connected to a non-terrestrial network.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT =
             "mms_max_ntn_payload_size_bytes_int";
 
@@ -9850,9 +9850,8 @@
      * manually scanning available cellular network.
      * If key is {@code true}, satellite plmn should not be exposed to user and should be
      * automatically set, {@code false} otherwise. Default value is {@code true}.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL =
             "remove_satellite_plmn_in_manual_network_scan_bool";
 
@@ -9877,18 +9876,18 @@
 
     /**
      * Doesn't support unrestricted traffic on satellite network.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED = 0;
     /**
      * Support unrestricted but bandwidth_constrained traffic on satellite network.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_DATA_SUPPORT_BANDWIDTH_CONSTRAINED = 1;
     /**
      * Support unrestricted satellite network that serves all traffic.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_DATA_SUPPORT_ALL = 2;
     /**
      * Indicates what kind of traffic an {@link NetworkCapabilities#NET_CAPABILITY_NOT_RESTRICTED}
@@ -9898,8 +9897,8 @@
      * {@link ApnSetting#INFRASTRUCTURE_SATELLITE} from APN infrastructure_bitmask, and this
      * configuration is ignored.
      * By default it only supports restricted data.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT =
             "satellite_data_support_mode_int";
 
@@ -9911,8 +9910,8 @@
      *                 {@link com.android.ims.ImsConfig.WfcModeFeatureValueConstants#WIFI_PREFERRED}
      * {@code false} - roaming preference can be changed by user independently and is not
      *                 overridden when device is connected to non-terrestrial network.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL =
             "override_wfc_roaming_mode_while_using_ntn_bool";
 
@@ -9945,8 +9944,8 @@
      * Reference: GSMA TS.43-v11, 2.8.5 Fast Authentication and Token Management.
      * `app_name` is an optional attribute in the request and may vary depending on the carrier
      * requirement.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING =
             "satellite_entitlement_app_name_string";
 
@@ -9954,9 +9953,8 @@
      * URL to redirect user to get more information about the carrier support for satellite.
      *
      * The default value is empty string.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING =
             "satellite_information_redirect_url_string";
     /**
@@ -9966,9 +9964,8 @@
      * This will need agreement with carriers before enabling this flag.
      *
      * The default value is false.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL =
             "emergency_messaging_supported_bool";
 
@@ -9983,9 +9980,8 @@
      * prompt user to switch to using satellite emergency messaging.
      *
      * The default value is 30 seconds.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT =
             "emergency_call_to_satellite_t911_handover_timeout_millis_int";
 
@@ -9998,9 +9994,8 @@
      * The default capabilities are
      * {@link NetworkRegistrationInfo#SERVICE_TYPE_SMS}, and
      * {@link NetworkRegistrationInfo#SERVICE_TYPE_MMS}
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY =
             "carrier_roaming_satellite_default_services_int_array";
 
@@ -10031,9 +10026,8 @@
      * Defines the NIDD (Non-IP Data Delivery) APN to be used for carrier roaming to satellite
      * attachment. For more on NIDD, see 3GPP TS 29.542.
      * Note this config is the only source of truth regarding the definition of the APN.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_SATELLITE_NIDD_APN_NAME_STRING =
             "satellite_nidd_apn_name_string";
 
@@ -10044,9 +10038,8 @@
      *
      * If {@code false}, the emergency call is always blocked if device is in emergency satellite
      * mode. Note if device is NOT in emergency satellite mode, emergency call is always allowed.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_SATELLITE_ROAMING_TURN_OFF_SESSION_FOR_EMERGENCY_CALL_BOOL =
             "satellite_roaming_turn_off_session_for_emergency_call_bool";
 
@@ -10059,14 +10052,14 @@
 
     /**
      * Device can connect to carrier roaming non-terrestrial network automatically.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC = 0;
     /**
      * Device can connect to carrier roaming non-terrestrial network only if user manually triggers
      * satellite connection.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int CARRIER_ROAMING_NTN_CONNECT_MANUAL = 1;
     /**
      * Indicates carrier roaming non-terrestrial network connect type that the device can use to
@@ -10074,8 +10067,8 @@
      * If this key is set to CARRIER_ROAMING_NTN_CONNECT_MANUAL then connect button will be
      * displayed to user when the device is eligible to use carrier roaming
      * non-terrestrial network.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT =
             "carrier_roaming_ntn_connect_type_int";
 
@@ -10088,7 +10081,6 @@
      * will be made to T911.
      *
      * The default value is {@link SatelliteManager#EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911}.
-     *
      */
     @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     public static final String
@@ -10105,9 +10097,8 @@
      * After the timer is expired, device is marked as eligible for satellite communication.
      *
      * The default value is 180 seconds.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT =
             "carrier_supported_satellite_notification_hysteresis_sec_int";
 
@@ -10153,7 +10144,9 @@
             "satellite_roaming_esos_inactivity_timeout_sec_int";
 
     /**
-     * A string array containing the list of messaging package names that support satellite.
+     * A string array containing the list of messaging apps that support satellite.
+     *
+     * The default value contains only "com.google.android.apps.messaging"
      *
      * @hide
      */
@@ -10166,9 +10159,8 @@
      * the default APN (i.e. internet) will be used for tethering.
      *
      * This config is only available when using Preset APN(not user edited) as Preferred APN.
-     *
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final String KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL =
             "disable_dun_apn_while_roaming_with_preset_apn_bool";
 
@@ -11313,6 +11305,8 @@
                         NetworkRegistrationInfo.SERVICE_TYPE_SMS,
                         NetworkRegistrationInfo.SERVICE_TYPE_MMS
                 });
+        sDefaults.putStringArray(KEY_SATELLITE_SUPPORTED_MSG_APPS_STRING_ARRAY, new String[]{
+                "com.google.android.apps.messaging"});
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
         sDefaults.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false);
         sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT,
diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.aidl b/telephony/java/android/telephony/satellite/EarfcnRange.aidl
new file mode 100644
index 0000000..0b224d0
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EarfcnRange.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.telephony.satellite;
+
+parcelable EarfcnRange;
diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.java b/telephony/java/android/telephony/satellite/EarfcnRange.java
new file mode 100644
index 0000000..38043b5
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EarfcnRange.java
@@ -0,0 +1,124 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.flags.Flags;
+
+/**
+ * EARFCN (E-UTRA Absolute Radio Frequency Channel Number):  A number that identifies a
+ * specific frequency channel in LTE/5G NR, used to define the carrier frequency.
+ * The range can be [0 ~ 65535] according to the 3GPP TS 36.101
+ *
+ * In satellite communication:
+ * - Efficient frequency allocation across a wide coverage area.
+ * - Handles Doppler shift due to satellite movement.
+ * - Manages interference with terrestrial networks.
+ *
+ * See 3GPP TS 36.101 and 38.101-1 for details.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public final class EarfcnRange implements Parcelable {
+
+    /**
+     * The start frequency of the earfcn range and is inclusive in the range
+     */
+    private int mStartEarfcn;
+
+    /**
+     * The end frequency of the earfcn range and is inclusive in the range.
+     */
+    private int mEndEarfcn;
+
+    private EarfcnRange(@NonNull Parcel in) {
+        readFromParcel(in);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mStartEarfcn);
+        dest.writeInt(mEndEarfcn);
+    }
+
+    private void readFromParcel(Parcel in) {
+        mStartEarfcn = in.readInt();
+        mEndEarfcn = in.readInt();
+    }
+
+    /**
+     * Constructor for the EarfcnRange class.
+     * The range can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @param startEarfcn The starting earfcn value.
+     * @param endEarfcn   The ending earfcn value.
+     */
+    public EarfcnRange(@IntRange(from = 0, to = 65535) int endEarfcn,
+            @IntRange(from = 0, to = 65535) int startEarfcn) {
+        mEndEarfcn = endEarfcn;
+        mStartEarfcn = startEarfcn;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return "startEarfcn: " + mStartEarfcn + ", " + "endEarfcn: " + mEndEarfcn;
+    }
+
+    @NonNull
+    public static final Creator<EarfcnRange> CREATOR = new Creator<EarfcnRange>() {
+        @Override
+        public EarfcnRange createFromParcel(Parcel in) {
+            return new EarfcnRange(in);
+        }
+
+        @Override
+        public EarfcnRange[] newArray(int size) {
+            return new EarfcnRange[size];
+        }
+    };
+
+    /**
+     * Returns the starting earfcn value for this range.
+     * It can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @return The starting earfcn.
+     */
+    public @IntRange(from = 0, to = 65535) int getStartEarfcn() {
+        return mStartEarfcn;
+    }
+
+    /**
+     * Returns the ending earfcn value for this range.
+     * It can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @return The ending earfcn.
+     */
+    public @IntRange(from = 0, to = 65535) int getEndEarfcn() {
+        return mEndEarfcn;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
index a7eda48..2730f90 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
@@ -16,6 +16,8 @@
 
 package android.telephony.satellite;
 
+import android.telephony.satellite.SatelliteAccessConfiguration;
+
 /**
  * Interface for satellite communication allowed state callback.
  * @hide
@@ -29,4 +31,14 @@
      * @param allowed whether satellite communication state or not
      */
     void onSatelliteCommunicationAllowedStateChanged(in boolean isAllowed);
+
+    /**
+     * Callback method invoked when the satellite access configuration changes
+     *
+     * @param The satellite access configuration associated with the current location.
+     * When satellite is not allowed at the current location,
+     * {@code satelliteRegionalConfiguration} will be null.
+     */
+    void onSatelliteAccessConfigurationChanged(in SatelliteAccessConfiguration
+        satelliteAccessConfiguration);
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl
new file mode 100644
index 0000000..0214193
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.telephony.satellite;
+
+ parcelable SatelliteAccessConfiguration;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java
new file mode 100644
index 0000000..c3ae70b
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.List;
+
+/**
+ * SatelliteAccessConfiguration is used to store satellite access configuration
+ * that will be applied to the satellite communication at the corresponding region.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public final class SatelliteAccessConfiguration implements Parcelable {
+    /**
+     * The list of satellites available at the current location.
+     */
+    @NonNull
+    private List<SatelliteInfo> mSatelliteInfoList;
+
+    /**
+     * The list of tag IDs associated with the current location
+     */
+    @NonNull
+    private int[] mTagIds;
+
+    /**
+     * Constructor for {@link SatelliteAccessConfiguration}.
+     *
+     * @param satelliteInfos The list of {@link SatelliteInfo} objects representing the satellites
+     *                       accessible with this configuration.
+     * @param tagIds         The list of tag IDs associated with this configuration.
+     */
+    public SatelliteAccessConfiguration(@NonNull List<SatelliteInfo> satelliteInfos,
+            @NonNull int[] tagIds) {
+        mSatelliteInfoList = satelliteInfos;
+        mTagIds = tagIds;
+    }
+
+    public SatelliteAccessConfiguration(Parcel in) {
+        mSatelliteInfoList = in.createTypedArrayList(SatelliteInfo.CREATOR);
+        mTagIds = new int[in.readInt()];
+        in.readIntArray(mTagIds);
+    }
+
+    public static final Creator<SatelliteAccessConfiguration> CREATOR =
+            new Creator<SatelliteAccessConfiguration>() {
+                @Override
+                public SatelliteAccessConfiguration createFromParcel(Parcel in) {
+                    return new SatelliteAccessConfiguration(in);
+                }
+
+                @Override
+                public SatelliteAccessConfiguration[] newArray(int size) {
+                    return new SatelliteAccessConfiguration[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedList(mSatelliteInfoList);
+        if (mTagIds != null && mTagIds.length > 0) {
+            dest.writeInt(mTagIds.length);
+            dest.writeIntArray(mTagIds);
+        } else {
+            dest.writeInt(0);
+        }
+    }
+
+    /**
+     * Returns a list of {@link SatelliteInfo} objects representing the satellites
+     * associated with this object.
+     *
+     * @return The list of {@link SatelliteInfo} objects.
+     */
+    @NonNull
+    public List<SatelliteInfo> getSatelliteInfos() {
+        return mSatelliteInfoList;
+    }
+
+    /**
+     * Returns a list of tag IDs associated with this object.
+     *
+     * @return The list of tag IDs.
+     */
+    @NonNull
+    public int[] getTagIds() {
+        return mTagIds;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
index 1a87020..bffb11f 100644
--- a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
@@ -17,6 +17,7 @@
 package android.telephony.satellite;
 
 import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
 
 import com.android.internal.telephony.flags.Flags;
 
@@ -40,4 +41,17 @@
      */
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     void onSatelliteCommunicationAllowedStateChanged(boolean isAllowed);
+
+    /**
+     * Callback method invoked when the satellite access configuration changes
+     *
+     * @param satelliteAccessConfiguration The satellite access configuration associated with
+     *                                       the current location. When satellite is not allowed at
+     *                                       the current location,
+     *                                       {@code satelliteRegionalConfiguration} will be null.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    default void onSatelliteAccessConfigurationChanged(
+            @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) {};
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.aidl b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl
new file mode 100644
index 0000000..fc2303b
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.telephony.satellite;
+
+ parcelable SatelliteInfo;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.java b/telephony/java/android/telephony/satellite/SatelliteInfo.java
new file mode 100644
index 0000000..bca907e
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteInfo.java
@@ -0,0 +1,169 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * SatelliteInfo stores a satellite's identification, position, and frequency information
+ * facilitating efficient satellite communications.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatelliteInfo implements Parcelable {
+    /**
+     * Unique identification number for the satellite.
+     * This ID is used to distinguish between different satellites in the network.
+     */
+    @NonNull
+    private UUID mId;
+
+    /**
+     * Position information of a satellite.
+     * This includes the longitude and altitude of the satellite.
+     */
+    private SatellitePosition mPosition;
+
+    /**
+     * The frequency bands to scan. Bands and earfcns won't overlap.
+     * Bands will be filled only if the whole band is needed.
+     * Maximum length of the vector is 8.
+     */
+    private int[] mBands;
+
+    /**
+     * EARFCN (E-UTRA Absolute Radio Frequency Channel Number) Ranges
+     * The supported frequency range list.
+     * Maximum length of the vector is 8.
+     */
+    private final List<EarfcnRange> mEarfcnRangeList;
+
+    protected SatelliteInfo(Parcel in) {
+        ParcelUuid parcelUuid = in.readParcelable(
+                ParcelUuid.class.getClassLoader(), ParcelUuid.class);
+        if (parcelUuid != null) {
+            mId = parcelUuid.getUuid();
+        }
+        mPosition = in.readParcelable(SatellitePosition.class.getClassLoader(),
+                SatellitePosition.class);
+        int numBands = in.readInt();
+        mBands = new int[numBands];
+        if (numBands > 0) {
+            for (int i = 0; i < numBands; i++) {
+                mBands[i] = in.readInt();
+            }
+        }
+        mEarfcnRangeList = in.createTypedArrayList(EarfcnRange.CREATOR);
+    }
+
+    /**
+     * Constructor for {@link SatelliteInfo}.
+     *
+     * @param satelliteId       The ID of the satellite.
+     * @param satellitePosition The {@link SatellitePosition} of the satellite.
+     * @param bands             The list of frequency bands supported by the satellite.
+     * @param earfcnRanges      The list of {@link EarfcnRange} objects representing the EARFCN
+     *                          ranges supported by the satellite.
+     */
+    public SatelliteInfo(@NonNull UUID satelliteId, @NonNull SatellitePosition satellitePosition,
+            @NonNull int[] bands, @NonNull List<EarfcnRange> earfcnRanges) {
+        mId = satelliteId;
+        mPosition = satellitePosition;
+        mBands = bands;
+        mEarfcnRangeList = earfcnRanges;
+    }
+
+    public static final Creator<SatelliteInfo> CREATOR = new Creator<SatelliteInfo>() {
+        @Override
+        public SatelliteInfo createFromParcel(Parcel in) {
+            return new SatelliteInfo(in);
+        }
+
+        @Override
+        public SatelliteInfo[] newArray(int size) {
+            return new SatelliteInfo[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(new ParcelUuid(mId), flags);
+        dest.writeParcelable(mPosition, flags);
+        if (mBands != null && mBands.length > 0) {
+            dest.writeInt(mBands.length);
+            dest.writeIntArray(mBands);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeTypedList(mEarfcnRangeList);
+    }
+
+    /**
+     * Returns the ID of the satellite.
+     *
+     * @return The satellite ID.
+     */
+    @NonNull
+    public UUID getSatelliteId() {
+        return mId;
+    }
+
+    /**
+     * Returns the position of the satellite.
+     *
+     * @return The {@link SatellitePosition} of the satellite.
+     */
+    public SatellitePosition getSatellitePosition() {
+        return mPosition;
+    }
+
+    /**
+     * Returns the list of frequency bands supported by the satellite.
+     *
+     * @return The list of frequency bands.
+     */
+    @NonNull
+    public int[] getBands() {
+        return mBands;
+    }
+
+    /**
+     * Returns the list of EARFCN ranges supported by the satellite.
+     *
+     * @return The list of {@link EarfcnRange} objects.
+     */
+    @NonNull
+    public List<EarfcnRange> getEarfcnRanges() {
+        return mEarfcnRangeList;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 7be3f33..7e3d99a 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -36,6 +36,7 @@
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyFrameworkInitializer;
@@ -272,6 +273,14 @@
     public static final String KEY_DEPROVISION_SATELLITE_TOKENS = "deprovision_satellite";
 
     /**
+     * Bundle key to get the response from
+     * {@link #requestSatelliteAccessConfigurationForCurrentLocation(Executor, OutcomeReceiver)}.
+     * @hide
+     */
+    public static final String KEY_SATELLITE_ACCESS_CONFIGURATION =
+            "satellite_access_configuration";
+
+    /**
      * The request was successfully processed.
      * @hide
      */
@@ -483,43 +492,43 @@
     /**
      * Telephony framework needs to access the current location of the device to perform the
      * request. However, location in the settings is disabled by users.
-     *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_RESULT_LOCATION_DISABLED = 25;
 
     /**
      * Telephony framework needs to access the current location of the device to perform the
      * request. However, Telephony fails to fetch the current location from location service.
-     *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26;
 
     /**
      * Emergency call is in progress.
-     *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS = 27;
 
     /**
      * Disabling satellite is in progress.
-     *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28;
 
     /**
      * Enabling satellite is in progress.
-     *
      * @hide
      */
-    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29;
 
     /** @hide */
@@ -715,7 +724,7 @@
     public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911 = 2;
 
     /**
-     * This intent will be broadcasted if there are any change to list of subscriber informations.
+     * This intent will be broadcasted if there are any change to list of subscriber information.
      * This intent will be sent only to the app with component defined in
      * config_satellite_carrier_roaming_esos_provisioned_class and package defined in
      * config_satellite_gateway_service_package
@@ -1349,12 +1358,16 @@
      * The satellite modem is being powered on.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8;
 
     /**
      * The satellite modem is being powered off.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9;
 
     /**
@@ -1414,6 +1427,8 @@
      * there is any incoming message.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3;
 
     /**
@@ -1421,6 +1436,8 @@
      * is the last message to emergency service provider indicating still needs help.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4;
 
     /**
@@ -1428,12 +1445,16 @@
      * is the last message to emergency service provider indicating no more help is needed.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5;
 
     /**
      * Datagram type indicating that the message to be sent or received is of type SMS.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int DATAGRAM_TYPE_SMS = 6;
 
     /**
@@ -1441,6 +1462,8 @@
      * for pending incoming SMS.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int DATAGRAM_TYPE_CHECK_PENDING_INCOMING_SMS = 7;
 
     /** @hide */
@@ -1461,6 +1484,8 @@
      * Satellite communication restricted by user.
      * @hide
      */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS)
     public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER = 0;
 
     /**
@@ -2316,6 +2341,68 @@
     }
 
     /**
+     * Request to get satellite access configuration for the current location.
+     *
+     * @param executor The executor on which the callback will be called.
+     * @param callback The callback object to which the result will be delivered.
+     *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
+     *                 will return a {@code SatelliteAccessConfiguration} with value the regional
+     *                 satellite access configuration at the current location.
+     *                 If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
+     *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public void requestSatelliteAccessConfigurationForCurrentLocation(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<SatelliteAccessConfiguration, SatelliteException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                ResultReceiver receiver = new ResultReceiver(null) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle resultData) {
+                        if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                            if (resultData.containsKey(KEY_SATELLITE_ACCESS_CONFIGURATION)) {
+                                SatelliteAccessConfiguration satelliteAccessConfiguration =
+                                        resultData.getParcelable(KEY_SATELLITE_ACCESS_CONFIGURATION,
+                                                SatelliteAccessConfiguration.class);
+                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                        callback.onResult(satelliteAccessConfiguration)));
+                            } else {
+                                loge("KEY_SATELLITE_ACCESS_CONFIGURATION does not exist.");
+                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                        callback.onError(new SatelliteException(
+                                                SATELLITE_RESULT_REQUEST_FAILED))));
+                            }
+                        } else {
+                            executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                    callback.onError(new SatelliteException(resultCode))));
+                        }
+                    }
+                };
+                telephony.requestSatelliteAccessConfigurationForCurrentLocation(receiver);
+            } else {
+                loge("requestSatelliteAccessConfigurationForCurrentLocation() invalid telephony");
+                executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+                        new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+            }
+        } catch (RemoteException ex) {
+            loge("requestSatelliteAccessConfigurationForCurrentLocation() RemoteException: "
+                    + ex);
+            executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+                    new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+        }
+    }
+
+    /**
      * Request to get the duration in seconds after which the satellite will be visible.
      * This will be {@link Duration#ZERO} if the satellite is currently visible.
      *
@@ -2420,7 +2507,7 @@
      * <li>There is no satellite communication restriction, which is added by
      * {@link #addAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li>
      * <li>The carrier config {@link
-     * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to
+     * CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to
      * {@code true}.</li>
      * </ul>
      *
@@ -2743,7 +2830,7 @@
      * <p>
      * Note: This API is specifically designed for OEM enabled satellite connectivity only.
      * For satellite connectivity enabled using carrier roaming, please refer to
-     * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and
+     * {@link TelephonyCallback.SignalStrengthsListener}, and
      * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}.
      * </p>
      *
@@ -2814,7 +2901,7 @@
      * <p>
      * Note: This API is specifically designed for OEM enabled satellite connectivity only.
      * For satellite connectivity enabled using carrier roaming, please refer to
-     * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and
+     * {@link TelephonyCallback.SignalStrengthsListener}, and
      * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}.
      * </p>
      *
@@ -3131,6 +3218,15 @@
                                         () -> callback.onSatelliteCommunicationAllowedStateChanged(
                                                 isAllowed)));
                             }
+
+                            @Override
+                            public void onSatelliteAccessConfigurationChanged(
+                                    @Nullable SatelliteAccessConfiguration
+                                            satelliteAccessConfiguration) {
+                                executor.execute(() -> Binder.withCleanCallingIdentity(
+                                        () -> callback.onSatelliteAccessConfigurationChanged(
+                                                satelliteAccessConfiguration)));
+                            }
                         };
                 sSatelliteCommunicationAllowedStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForCommunicationAllowedStateChanged(
diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.aidl b/telephony/java/android/telephony/satellite/SatellitePosition.aidl
new file mode 100644
index 0000000..a8028eb
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatellitePosition.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 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.telephony.satellite;
+
+ parcelable SatellitePosition;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.java b/telephony/java/android/telephony/satellite/SatellitePosition.java
new file mode 100644
index 0000000..1e8c018
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatellitePosition.java
@@ -0,0 +1,114 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+/**
+ * The position of a satellite in Earth orbit.
+ *
+ * Longitude is the angular distance, measured in degrees, east or west of the prime longitude line
+ * ranging from -180 to 180 degrees
+ * Altitude is the distance from the center of the Earth to the satellite, measured in kilometers
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatellitePosition implements Parcelable {
+
+    /**
+     * The longitude of the satellite in degrees, ranging from -180 to 180 degrees
+     */
+    private double mLongitudeDegree;
+
+    /**
+     * The distance from the center of the earth to the satellite, measured in kilometers
+     */
+    private double mAltitudeKm;
+
+    /**
+     * Constructor for {@link SatellitePosition} used to create an instance from a {@link Parcel}.
+     *
+     * @param in The {@link Parcel} to read the satellite position data from.
+     */
+    public SatellitePosition(Parcel in) {
+        mLongitudeDegree = in.readDouble();
+        mAltitudeKm = in.readDouble();
+    }
+
+    /**
+     * Constructor for {@link SatellitePosition}.
+     *
+     * @param longitudeDegree The longitude of the satellite in degrees.
+     * @param altitudeKm  The altitude of the satellite in kilometers.
+     */
+    public SatellitePosition(double longitudeDegree, double altitudeKm) {
+        mLongitudeDegree = longitudeDegree;
+        mAltitudeKm = altitudeKm;
+    }
+
+    public static final Creator<SatellitePosition> CREATOR = new Creator<SatellitePosition>() {
+        @Override
+        public SatellitePosition createFromParcel(Parcel in) {
+            return new SatellitePosition(in);
+        }
+
+        @Override
+        public SatellitePosition[] newArray(int size) {
+            return new SatellitePosition[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeDouble(mLongitudeDegree);
+        dest.writeDouble(mAltitudeKm);
+    }
+
+    /**
+     * Returns the longitude of the satellite in degrees, ranging from -180 to 180 degrees.
+     *
+     * @return The longitude of the satellite.
+     */
+    public double getLongitudeDegrees() {
+        return mLongitudeDegree;
+    }
+
+    /**
+     * Returns the altitude of the satellite in kilometers
+     *
+     * @return The altitude of the satellite.
+     */
+    public double getAltitudeKm() {
+        return mAltitudeKm;
+    }
+}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 62cbb02..210200b 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -886,7 +886,7 @@
     /**
     *  @return true if the ImsService to bind to for the slot id specified was set, false otherwise.
     */
-    boolean setBoundImsServiceOverride(int slotIndex, boolean isCarrierService,
+    boolean setBoundImsServiceOverride(int slotIndex, int userId, boolean isCarrierService,
             in int[] featureTypes, in String packageName);
 
     /**
@@ -2999,6 +2999,16 @@
     void requestIsCommunicationAllowedForCurrentLocation(int subId, in ResultReceiver receiver);
 
     /**
+     * Request to get satellite access configuration for the current location.
+     *
+     * @param receiver Result receiver to get the error code of the request
+     *                 and satellite access configuration for the current location.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    void requestSatelliteAccessConfigurationForCurrentLocation(in ResultReceiver receiver);
+
+    /**
      * Request to get the time after which the satellite will be visible.
      *
      * @param receiver Result receiver to get the error code of the request and the requested
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 332b9b8..9a9a331 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -21,7 +21,7 @@
 import android.graphics.Rect
 import android.graphics.Region
 import android.os.SystemClock
-import android.platform.uiautomator_helpers.DeviceHelpers
+import android.platform.uiautomatorhelpers.DeviceHelpers
 import android.tools.device.apphelpers.IStandardAppHelper
 import android.tools.helpers.SYSTEMUI_PACKAGE
 import android.tools.traces.parsers.WindowManagerStateHelper
@@ -159,7 +159,7 @@
     ) {
         val caption = getCaptionForTheApp(wmHelper, device)
         val maximizeButton = getMaximizeButtonForTheApp(caption)
-        maximizeButton?.longClick()
+        maximizeButton.longClick()
         wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
 
         val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 1574d1b..09a686c 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -757,7 +757,55 @@
                 intArrayOf(KeyEvent.KEYCODE_MINUS),
                 KeyEvent.META_ALT_ON,
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
-            )
+            ),
+            TestData(
+                "META + ALT + '-' -> Magnifier Zoom Out",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_MINUS
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
+                intArrayOf(KeyEvent.KEYCODE_MINUS),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + '=' -> Magnifier Zoom In",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_EQUALS
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
+                intArrayOf(KeyEvent.KEYCODE_EQUALS),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + M -> Toggle Magnification",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_M
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+                intArrayOf(KeyEvent.KEYCODE_M),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + S -> Activate Select to Speak",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_S
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
+                intArrayOf(KeyEvent.KEYCODE_S),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }
 
@@ -770,6 +818,7 @@
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
+        com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
         com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
         com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
     )
@@ -787,6 +836,7 @@
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG,
         com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS,
+        com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES,
         com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT,
         com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS
     )
@@ -995,11 +1045,28 @@
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE),
                 AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR)
             ),
+            TestData(
+                "LOCK -> Lock Screen",
+                intArrayOf(KeyEvent.KEYCODE_LOCK),
+                KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN,
+                intArrayOf(KeyEvent.KEYCODE_LOCK),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "FULLSCREEN -> Maximizes a task to fit the screen",
+                intArrayOf(KeyEvent.KEYCODE_FULLSCREEN),
+                KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+                intArrayOf(KeyEvent.KEYCODE_FULLSCREEN),
+                0,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }
 
     @Test
     @Parameters(method = "systemKeysTestArguments")
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES)
     fun testSystemKeys(test: TestData) {
         setupKeyGestureController()
         testKeyGestureInternal(test)
@@ -1029,6 +1096,9 @@
             KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
             KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
             KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
+            KeyEvent.KEYCODE_DO_NOT_DISTURB,
+            KeyEvent.KEYCODE_LOCK,
+            KeyEvent.KEYCODE_FULLSCREEN
         )
 
         val handler = KeyGestureHandler { _, _ -> false }
diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
index 07b7338..0da4521 100644
--- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
+++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java
@@ -143,6 +143,38 @@
     }
 
     @Test
+    public void testStartVendorVibrationSessionWithoutVibratePermissionFails() throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE_VENDOR_EFFECTS,
+                Manifest.permission.START_VIBRATION_SESSIONS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
+    public void testStartVendorVibrationSessionWithoutVibrateVendorEffectsPermissionFails()
+            throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE,
+                Manifest.permission.START_VIBRATION_SESSIONS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
+    public void testStartVendorVibrationSessionWithoutStartSessionPermissionFails()
+            throws Exception {
+        getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.VIBRATE,
+                Manifest.permission.VIBRATE_VENDOR_EFFECTS);
+        expectSecurityException("VIBRATE");
+        mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME,
+                new int[] { 1 }, ATTRS, "testVibrate", null);
+    }
+
+    @Test
     public void testCancelVibrateFails() throws RemoteException {
         expectSecurityException("VIBRATE");
         mVibratorService.cancelVibrate(/* usageFilter= */ -1, new Binder());
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index 1abe77f..f260e27 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -188,7 +188,7 @@
                         ?: throw IllegalArgumentException(
                             "Invalid feature version input for $name: ${featureArgs[1]}"
                         )
-                FeatureInfo(name, featureArgs[1].toInt(), readonly = true)
+                FeatureInfo(name, featureVersion, readonly = true)
             }
         }
     }