Merge "Remove server side task positioner" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index a16aa2d..5ef7759 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -98,6 +98,7 @@
         "framework-jobscheduler-job.flags-aconfig-java",
         "framework_graphics_flags_java_lib",
         "hwui_flags_java_lib",
+        "libcore_exported_aconfig_flags_lib",
         "power_flags_lib",
         "sdk_sandbox_flags_lib",
         "surfaceflinger_flags_java_lib",
@@ -140,6 +141,14 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// Core Libraries / libcore
+java_aconfig_library {
+    name: "libcore_exported_aconfig_flags_lib",
+    aconfig_declarations: "libcore-aconfig-flags",
+    mode: "exported",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Telecom
 java_aconfig_library {
     name: "telecom_flags_core_java_lib",
@@ -363,6 +372,7 @@
     min_sdk_version: "30",
     apex_available: [
         "//apex_available:platform",
+        "com.android.btservices",
         "com.android.mediaprovider",
         "com.android.permission",
     ],
@@ -403,17 +413,6 @@
 cc_aconfig_library {
     name: "android.companion.virtualdevice.flags-aconfig-cc",
     aconfig_declarations: "android.companion.virtualdevice.flags-aconfig",
-}
-
-cc_aconfig_library {
-    name: "android.companion.virtualdevice.flags-aconfig-cc-host",
-    aconfig_declarations: "android.companion.virtualdevice.flags-aconfig",
-    host_supported: true,
-}
-
-cc_aconfig_library {
-    name: "android.companion.virtualdevice.flags-aconfig-cc-test",
-    aconfig_declarations: "android.companion.virtualdevice.flags-aconfig",
     host_supported: true,
     mode: "test",
 }
@@ -1490,6 +1489,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "backstage_power_flags_lib-host",
+    aconfig_declarations: "backstage_power_flags",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // Dropbox data
 aconfig_declarations {
     name: "dropbox_flags",
diff --git a/Android.bp b/Android.bp
index f0aa62c..eabd9c7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -417,7 +417,6 @@
         "modules-utils-fastxmlserializer",
         "modules-utils-preconditions",
         "modules-utils-statemachine",
-        "modules-utils-synchronous-result-receiver",
         "modules-utils-os",
         "modules-utils-uieventlogger-interface",
         "framework-permission-aidl-java",
diff --git a/apct-tests/perftests/multiuser/Android.bp b/apct-tests/perftests/multiuser/Android.bp
index 1653edc..856dba3 100644
--- a/apct-tests/perftests/multiuser/Android.bp
+++ b/apct-tests/perftests/multiuser/Android.bp
@@ -38,3 +38,10 @@
     ],
     certificate: "platform",
 }
+
+filegroup {
+    name: "multi_user_trace_config",
+    srcs: [
+        "trace_configs/trace_config_multi_user.textproto",
+    ],
+}
diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java
index 4bcc8c4..f302033 100644
--- a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java
+++ b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java
@@ -31,6 +31,7 @@
 import android.content.pm.PackageManager;
 import android.perftests.utils.BenchmarkState;
 import android.perftests.utils.PerfStatusReporter;
+import android.permission.PermissionManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -107,6 +108,8 @@
     public void setup() {
         PackageManager.disableApplicationInfoCache();
         PackageManager.disablePackageInfoCache();
+        PermissionManager.disablePermissionCache();
+        PermissionManager.disablePackageNamePermissionCache();
     }
 
     @Test
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index dc5e341..93904a7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.os.Environment;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -113,15 +114,18 @@
     }
 
     /**
-     * Add user wakeup for the alarm.
+     * Add user wakeup for the alarm if needed.
      * @param userId Id of the user that scheduled alarm.
      * @param alarmTime time when alarm is expected to trigger.
      */
     public void addUserWakeup(int userId, long alarmTime) {
-        synchronized (mUserWakeupLock) {
-            mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+        // SYSTEM user is always running, so no need to schedule wakeup for it.
+        if (userId != UserHandle.USER_SYSTEM) {
+            synchronized (mUserWakeupLock) {
+                mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+            }
+            updateUserListFile();
         }
-        updateUserListFile();
     }
 
     /**
diff --git a/api/Android.bp b/api/Android.bp
index 6a04f0d..d931df1 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -29,12 +29,14 @@
     pkgPath: "android/soong/api",
     deps: [
         "blueprint",
+        "blueprint-proptools",
         "soong",
         "soong-android",
         "soong-genrule",
         "soong-java",
     ],
     srcs: ["api.go"],
+    testSrcs: ["api_test.go"],
     pluginFor: ["soong_build"],
 }
 
@@ -60,40 +62,8 @@
 metalava_cmd += " -J--add-opens=java.base/java.util=ALL-UNNAMED "
 metalava_cmd += " --quiet "
 
-soong_config_module_type {
-    name: "enable_crashrecovery_module",
-    module_type: "combined_apis_defaults",
-    config_namespace: "ANDROID",
-    bool_variables: ["release_crashrecovery_module"],
-    properties: [
-        "bootclasspath",
-        "system_server_classpath",
-    ],
-}
-
-soong_config_bool_variable {
-    name: "release_crashrecovery_module",
-}
-
-enable_crashrecovery_module {
-    name: "crashrecovery_module_defaults",
-    soong_config_variables: {
-        release_crashrecovery_module: {
-            bootclasspath: [
-                "framework-crashrecovery",
-            ],
-            system_server_classpath: [
-                "service-crashrecovery",
-            ],
-        },
-    },
-}
-
 combined_apis {
     name: "frameworks-base-api",
-    defaults: [
-        "crashrecovery_module_defaults",
-    ],
     bootclasspath: [
         "android.net.ipsec.ike",
         "art.module.public.api",
@@ -126,7 +96,12 @@
         "framework-virtualization",
         "framework-wifi",
         "i18n.module.public.api",
-    ],
+    ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
+        "true": [
+            "framework-crashrecovery",
+        ],
+        default: [],
+    }),
     system_server_classpath: [
         "service-art",
         "service-configinfrastructure",
@@ -135,7 +110,12 @@
         "service-permission",
         "service-rkp",
         "service-sdksandbox",
-    ],
+    ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
+        "true": [
+            "service-crashrecovery",
+        ],
+        default: [],
+    }),
 }
 
 genrule {
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 12820f9..8dfddf0 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -1345,4 +1345,5 @@
         ":hwbinder-stubs-docs",
     ],
     visibility: ["//visibility:public"],
+    is_stubs_module: true,
 }
diff --git a/api/api.go b/api/api.go
index d4db49e..b6b1a7e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -54,16 +54,15 @@
 // The properties of the combined_apis module type.
 type CombinedApisProperties struct {
 	// Module libraries in the bootclasspath
-	Bootclasspath []string
+	Bootclasspath proptools.Configurable[[]string]
 	// Module libraries on the bootclasspath if include_nonpublic_framework_api is true.
 	Conditional_bootclasspath []string
 	// Module libraries in system server
-	System_server_classpath []string
+	System_server_classpath proptools.Configurable[[]string]
 }
 
 type CombinedApis struct {
 	android.ModuleBase
-	android.DefaultableModuleBase
 
 	properties CombinedApisProperties
 }
@@ -74,34 +73,41 @@
 
 func registerBuildComponents(ctx android.RegistrationContext) {
 	ctx.RegisterModuleType("combined_apis", combinedApisModuleFactory)
-	ctx.RegisterModuleType("combined_apis_defaults", CombinedApisModuleDefaultsFactory)
 }
 
 var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents)
 
-func (a *CombinedApis) apiFingerprintStubDeps() []string {
+func (a *CombinedApis) bootclasspath(ctx android.ConfigAndErrorContext) []string {
+	return a.properties.Bootclasspath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil)
+}
+
+func (a *CombinedApis) systemServerClasspath(ctx android.ConfigAndErrorContext) []string {
+	return a.properties.System_server_classpath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil)
+}
+
+func (a *CombinedApis) apiFingerprintStubDeps(ctx android.BottomUpMutatorContext) []string {
 	ret := []string{}
 	ret = append(
 		ret,
-		transformArray(a.properties.Bootclasspath, "", ".stubs")...,
+		transformArray(a.bootclasspath(ctx), "", ".stubs")...,
 	)
 	ret = append(
 		ret,
-		transformArray(a.properties.Bootclasspath, "", ".stubs.system")...,
+		transformArray(a.bootclasspath(ctx), "", ".stubs.system")...,
 	)
 	ret = append(
 		ret,
-		transformArray(a.properties.Bootclasspath, "", ".stubs.module_lib")...,
+		transformArray(a.bootclasspath(ctx), "", ".stubs.module_lib")...,
 	)
 	ret = append(
 		ret,
-		transformArray(a.properties.System_server_classpath, "", ".stubs.system_server")...,
+		transformArray(a.systemServerClasspath(ctx), "", ".stubs.system_server")...,
 	)
 	return ret
 }
 
 func (a *CombinedApis) DepsMutator(ctx android.BottomUpMutatorContext) {
-	ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps()...)
+	ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps(ctx)...)
 }
 
 func (a *CombinedApis) GenerateAndroidBuildActions(ctx android.ModuleContext) {
@@ -532,8 +538,8 @@
 }
 
 func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) {
-	bootclasspath := a.properties.Bootclasspath
-	system_server_classpath := a.properties.System_server_classpath
+	bootclasspath := a.bootclasspath(ctx)
+	system_server_classpath := a.systemServerClasspath(ctx)
 	if ctx.Config().VendorConfig("ANDROID").Bool("include_nonpublic_framework_api") {
 		bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...)
 		sort.Strings(bootclasspath)
@@ -568,7 +574,6 @@
 	module := &CombinedApis{}
 	module.AddProperties(&module.properties)
 	android.InitAndroidModule(module)
-	android.InitDefaultableModule(module)
 	android.AddLoadHook(module, func(ctx android.LoadHookContext) { module.createInternalModules(ctx) })
 	return module
 }
@@ -605,16 +610,3 @@
 	}
 	return s2
 }
-
-// Defaults
-type CombinedApisModuleDefaults struct {
-	android.ModuleBase
-	android.DefaultsModuleBase
-}
-
-func CombinedApisModuleDefaultsFactory() android.Module {
-	module := &CombinedApisModuleDefaults{}
-	module.AddProperties(&CombinedApisProperties{})
-	android.InitDefaultsModule(module)
-	return module
-}
diff --git a/api/api_test.go b/api/api_test.go
new file mode 100644
index 0000000..47d1670
--- /dev/null
+++ b/api/api_test.go
@@ -0,0 +1,254 @@
+// 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 api
+
+import (
+	"android/soong/android"
+	"android/soong/java"
+	"fmt"
+	"testing"
+
+	"github.com/google/blueprint/proptools"
+)
+
+var prepareForTestWithCombinedApis = android.GroupFixturePreparers(
+	android.FixtureRegisterWithContext(registerBuildComponents),
+	java.PrepareForTestWithJavaBuildComponents,
+	android.FixtureAddTextFile("a/Android.bp", gatherRequiredDepsForTest()),
+	java.PrepareForTestWithJavaSdkLibraryFiles,
+	android.FixtureMergeMockFs(android.MockFS{
+		"a/api/current.txt":            nil,
+		"a/api/removed.txt":            nil,
+		"a/api/system-current.txt":     nil,
+		"a/api/system-removed.txt":     nil,
+		"a/api/test-current.txt":       nil,
+		"a/api/test-removed.txt":       nil,
+		"a/api/module-lib-current.txt": nil,
+		"a/api/module-lib-removed.txt": nil,
+	}),
+	android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
+		variables.Allow_missing_dependencies = proptools.BoolPtr(true)
+	}),
+)
+
+func gatherRequiredDepsForTest() string {
+	var bp string
+
+	extraLibraryModules := []string{
+		"stable.core.platform.api.stubs",
+		"core-lambda-stubs",
+		"core.current.stubs",
+		"ext",
+		"framework",
+		"android_stubs_current.from-text",
+		"android_system_stubs_current.from-text",
+		"android_test_stubs_current.from-text",
+		"android_test_frameworks_core_stubs_current.from-text",
+		"android_module_lib_stubs_current.from-text",
+		"android_system_server_stubs_current.from-text",
+		"android_stubs_current.from-source",
+		"android_system_stubs_current.from-source",
+		"android_test_stubs_current.from-source",
+		"android_test_frameworks_core_stubs_current.from-source",
+		"android_module_lib_stubs_current.from-source",
+		"android_system_server_stubs_current.from-source",
+		"android_stubs_current_exportable.from-source",
+		"android_system_stubs_current_exportable.from-source",
+		"android_test_stubs_current_exportable.from-source",
+		"android_module_lib_stubs_current_exportable.from-source",
+		"android_system_server_stubs_current_exportable.from-source",
+		"stub-annotations",
+	}
+
+	extraSdkLibraryModules := []string{
+		"framework-virtualization",
+		"framework-location",
+	}
+
+	extraSystemModules := []string{
+		"core-public-stubs-system-modules",
+		"core-module-lib-stubs-system-modules",
+		"stable-core-platform-api-stubs-system-modules",
+	}
+
+	extraFilegroupModules := []string{
+		"non-updatable-current.txt",
+		"non-updatable-removed.txt",
+		"non-updatable-system-current.txt",
+		"non-updatable-system-removed.txt",
+		"non-updatable-test-current.txt",
+		"non-updatable-test-removed.txt",
+		"non-updatable-module-lib-current.txt",
+		"non-updatable-module-lib-removed.txt",
+		"non-updatable-system-server-current.txt",
+		"non-updatable-system-server-removed.txt",
+		"non-updatable-exportable-current.txt",
+		"non-updatable-exportable-removed.txt",
+		"non-updatable-exportable-system-current.txt",
+		"non-updatable-exportable-system-removed.txt",
+		"non-updatable-exportable-test-current.txt",
+		"non-updatable-exportable-test-removed.txt",
+		"non-updatable-exportable-module-lib-current.txt",
+		"non-updatable-exportable-module-lib-removed.txt",
+		"non-updatable-exportable-system-server-current.txt",
+		"non-updatable-exportable-system-server-removed.txt",
+	}
+
+	for _, extra := range extraLibraryModules {
+		bp += fmt.Sprintf(`
+			java_library {
+				name: "%s",
+				srcs: ["a.java"],
+				sdk_version: "none",
+				system_modules: "stable-core-platform-api-stubs-system-modules",
+				compile_dex: true,
+			}
+		`, extra)
+	}
+
+	for _, extra := range extraSdkLibraryModules {
+		bp += fmt.Sprintf(`
+			java_sdk_library {
+				name: "%s",
+				srcs: ["a.java"],
+				public: {
+					enabled: true,
+				},
+				system: {
+					enabled: true,
+				},
+				test: {
+					enabled: true,
+				},
+				module_lib: {
+					enabled: true,
+				},
+				api_packages: [
+					"foo",
+				],
+				sdk_version: "core_current",
+				compile_dex: true,
+				annotations_enabled: true,
+			}
+		`, extra)
+	}
+
+	for _, extra := range extraFilegroupModules {
+		bp += fmt.Sprintf(`
+			filegroup {
+				name: "%[1]s",
+			}
+		`, extra)
+	}
+
+	for _, extra := range extraSystemModules {
+		bp += fmt.Sprintf(`
+			java_system_modules {
+				name: "%[1]s",
+				libs: ["%[1]s-lib"],
+			}
+			java_library {
+				name: "%[1]s-lib",
+				sdk_version: "none",
+				system_modules: "none",
+			}
+		`, extra)
+	}
+
+	bp += fmt.Sprintf(`
+		java_defaults {
+			name: "android.jar_defaults",
+		}
+	`)
+
+	return bp
+}
+
+func TestCombinedApisDefaults(t *testing.T) {
+
+	result := android.GroupFixturePreparers(
+		prepareForTestWithCombinedApis,
+		java.FixtureWithLastReleaseApis(
+			"framework-location", "framework-virtualization", "framework-foo", "framework-bar"),
+		android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
+			variables.VendorVars = map[string]map[string]string{
+				"boolean_var": {
+					"for_testing": "true",
+				},
+			}
+		}),
+	).RunTestWithBp(t, `
+		java_sdk_library {
+			name: "framework-foo",
+			srcs: ["a.java"],
+			public: {
+				enabled: true,
+			},
+			system: {
+				enabled: true,
+			},
+			test: {
+				enabled: true,
+			},
+			module_lib: {
+				enabled: true,
+			},
+			api_packages: [
+				"foo",
+			],
+			sdk_version: "core_current",
+			annotations_enabled: true,
+		}
+
+		java_sdk_library {
+			name: "framework-bar",
+			srcs: ["a.java"],
+			public: {
+				enabled: true,
+			},
+			system: {
+				enabled: true,
+			},
+			test: {
+				enabled: true,
+			},
+			module_lib: {
+				enabled: true,
+			},
+			api_packages: [
+				"foo",
+			],
+			sdk_version: "core_current",
+			annotations_enabled: true,
+		}
+
+		combined_apis {
+			name: "foo",
+			bootclasspath: [
+				"framework-bar",
+			] + select(boolean_var_for_testing(), {
+				true: [
+					"framework-foo",
+				],
+				default: [],
+			}),
+		}
+	`)
+
+	subModuleDependsOnSelectAppendedModule := java.CheckModuleHasDependency(t,
+		result.TestContext, "foo-current.txt", "", "framework-foo")
+	android.AssertBoolEquals(t, "Submodule expected to depend on the select-appended module",
+		true, subModuleDependsOnSelectAppendedModule)
+}
diff --git a/api/go.work b/api/go.work
index edd002e..c09bee5 100644
--- a/api/go.work
+++ b/api/go.work
@@ -1,17 +1,17 @@
-go 1.18
+go 1.22
 
 use (
 	.
-	../../../build/soong
 	../../../build/blueprint
+	../../../build/soong
 	../../../external/go-cmp
 	../../../external/golang-protobuf
 )
 
 replace (
 	android/soong v0.0.0 => ../../../build/soong
-	google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf
 	github.com/google/blueprint v0.0.0 => ../../../build/blueprint
 	github.com/google/go-cmp v0.0.0 => ../../../external/go-cmp
 	go.starlark.net v0.0.0 => ../../../external/starlark-go
+	google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf
 )
diff --git a/core/api/current.txt b/core/api/current.txt
index 0f23721..69c409b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26569,7 +26569,7 @@
 package android.media.projection {
 
   public final class MediaProjection {
-    method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
+    method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler);
     method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler);
     method public void stop();
     method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback);
@@ -54862,6 +54862,8 @@
     method @Deprecated public void addAction(int);
     method public void addChild(android.view.View);
     method public void addChild(android.view.View, int);
+    method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View);
+    method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View, int);
     method public boolean canOpenPopup();
     method public int describeContents();
     method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String);
@@ -54890,6 +54892,7 @@
     method public int getInputType();
     method public android.view.accessibility.AccessibilityNodeInfo getLabelFor();
     method 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();
     method public int getMaxTextLength();
     method @NonNull public java.time.Duration getMinDurationBetweenContentChanges();
@@ -54950,6 +54953,8 @@
     method public boolean removeAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction);
     method public boolean removeChild(android.view.View);
     method public boolean removeChild(android.view.View, int);
+    method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View);
+    method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View, int);
     method public void setAccessibilityDataSensitive(boolean);
     method public void setAccessibilityFocused(boolean);
     method public void setAvailableExtraData(java.util.List<java.lang.String>);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 44c4ab4..88b5275 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -423,6 +423,7 @@
   public final class PictureInPictureParams implements android.os.Parcelable {
     method public float getAspectRatioFloat();
     method public float getExpandedAspectRatioFloat();
+    method public static boolean isSameAspectRatio(@NonNull android.graphics.Rect, @NonNull android.util.Rational);
   }
 
   public final class PictureInPictureUiState implements android.os.Parcelable {
@@ -618,6 +619,7 @@
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void resetDefaultCrossProfileIntentFilters(int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void resetShouldAllowBypassingDevicePolicyManagementRoleQualificationState();
     method @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int);
+    method @FlaggedApi("android.app.admin.flags.provisioning_context_parameter") @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int, @Nullable String);
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwner(@NonNull android.content.ComponentName, int);
     method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int);
     method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int);
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index 845a346..ac37113 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1381,6 +1381,18 @@
             }
             int toId = findLatestEventIdForTime(playTime);
             handleAnimationEvents(-1, toId, playTime);
+
+            if (mSeekState.isActive()) {
+                // Pump a frame to the on-going animators
+                for (int i = 0; i < mPlayingSet.size(); i++) {
+                    Node node = mPlayingSet.get(i);
+                    if (!node.mEnded) {
+                        pulseFrame(node, getPlayTimeForNodeIncludingDelay(playTime, node));
+                    }
+                }
+            }
+
+            // Remove all the finished anims
             for (int i = mPlayingSet.size() - 1; i >= 0; i--) {
                 if (mPlayingSet.get(i).mEnded) {
                     mPlayingSet.remove(i);
diff --git a/core/java/android/animation/OWNERS b/core/java/android/animation/OWNERS
index f3b330a..5223c87 100644
--- a/core/java/android/animation/OWNERS
+++ b/core/java/android/animation/OWNERS
@@ -3,3 +3,4 @@
 romainguy@google.com
 tianliu@google.com
 adamp@google.com
+mount@google.com
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index c6a1546..65acd49 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -104,7 +104,9 @@
             MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED,
             MODE_BACKGROUND_ACTIVITY_START_ALLOWED,
             MODE_BACKGROUND_ACTIVITY_START_DENIED,
-            MODE_BACKGROUND_ACTIVITY_START_COMPAT})
+            MODE_BACKGROUND_ACTIVITY_START_COMPAT,
+            MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS,
+            MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE})
     public @interface BackgroundActivityStartMode {}
     /**
      * No explicit value chosen. The system will decide whether to grant privileges.
@@ -119,6 +121,20 @@
      */
     public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2;
     /**
+     * Allow the {@link PendingIntent} to use ALL background activity start privileges, including
+     * special permissions that will allow starts at any time.
+     *
+     * @hide
+     */
+    public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3;
+    /**
+     * Allow the {@link PendingIntent} to use background activity start privileges based on
+     * visibility of the app.
+     *
+     * @hide
+     */
+    public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4;
+    /**
      * Special behavior for compatibility.
      * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED}
      *
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 36b1eab..6df971a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -2105,8 +2105,7 @@
         @Override
         public void scheduleTaskFragmentTransaction(@NonNull ITaskFragmentOrganizer organizer,
                 @NonNull TaskFragmentTransaction transaction) throws RemoteException {
-            // TODO(b/260873529): ITaskFragmentOrganizer can be cleanup to be a IBinder token
-            // after flag removal.
+            // TODO(b/352665082): ITaskFragmentOrganizer can be cleanup to be a IBinder token
             organizer.onTransactionReady(transaction);
         }
 
diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java
index 0e8e2e3..b3fc058 100644
--- a/core/java/android/app/ComponentOptions.java
+++ b/core/java/android/app/ComponentOptions.java
@@ -18,9 +18,11 @@
 
 import static android.app.ActivityOptions.BackgroundActivityStartMode;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -48,15 +50,7 @@
     public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED =
             "android.pendingIntent.backgroundActivityAllowed";
 
-    /**
-     * PendingIntent caller allows activity to be started if caller has BAL permission.
-     * @hide
-     */
-    public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION =
-            "android.pendingIntent.backgroundActivityAllowedByPermission";
-
     private Integer mPendingIntentBalAllowed = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
-    private boolean mPendingIntentBalAllowedByPermission = false;
 
     ComponentOptions() {
     }
@@ -69,9 +63,6 @@
         mPendingIntentBalAllowed =
                 opts.getInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED,
                         MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
-        setPendingIntentBackgroundActivityLaunchAllowedByPermission(
-                opts.getBoolean(
-                        KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, false));
     }
 
     /**
@@ -114,10 +105,19 @@
     public @NonNull ComponentOptions setPendingIntentBackgroundActivityStartMode(
             @BackgroundActivityStartMode int state) {
         switch (state) {
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOWED:
+                if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) {
+                    // do not overwrite ALWAYS with ALLOWED for backwards compatibility,
+                    // if setPendingIntentBackgroundActivityLaunchAllowedByPermission is used
+                    // before this method.
+                    mPendingIntentBalAllowed = state;
+                }
+                break;
             case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
             case MODE_BACKGROUND_ACTIVITY_START_DENIED:
             case MODE_BACKGROUND_ACTIVITY_START_COMPAT:
-            case MODE_BACKGROUND_ACTIVITY_START_ALLOWED:
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS:
+            case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE:
                 mPendingIntentBalAllowed = state;
                 break;
             default:
@@ -140,20 +140,32 @@
     }
 
     /**
-     * Set PendingIntent activity can be launched from background if caller has BAL permission.
-     * @hide
-     */
-    public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) {
-        mPendingIntentBalAllowedByPermission = allowed;
-    }
-
-    /**
      * Get PendingIntent activity is allowed to be started in the background if the caller
      * has BAL permission.
      * @hide
+     * @deprecated check for #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
      */
+    @Deprecated
     public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() {
-        return mPendingIntentBalAllowedByPermission;
+        return mPendingIntentBalAllowed == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
+    }
+
+    /**
+     * Set PendingIntent activity can be launched from background if caller has BAL permission.
+     * @hide
+     * @deprecated use #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
+     */
+    @Deprecated
+    public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) {
+        if (allowed) {
+            setPendingIntentBackgroundActivityStartMode(
+                    MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+        } else {
+            if (getPendingIntentBackgroundActivityStartMode()
+                    == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) {
+                setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+            }
+        }
     }
 
     /** @hide */
@@ -162,10 +174,6 @@
         if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
             b.putInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed);
         }
-        if (mPendingIntentBalAllowedByPermission) {
-            b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION,
-                    mPendingIntentBalAllowedByPermission);
-        }
         return b;
     }
 
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 0fad979..1200b4b 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -118,6 +118,8 @@
 per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS
+
 
 # Multitasking
 per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/app/PictureInPictureParams.java b/core/java/android/app/PictureInPictureParams.java
index 96d874e..afe915e 100644
--- a/core/java/android/app/PictureInPictureParams.java
+++ b/core/java/android/app/PictureInPictureParams.java
@@ -654,6 +654,33 @@
                 && !hasSetSubtitle() && mIsLaunchIntoPip == null;
     }
 
+    /**
+     * Compare a given {@link Rect} against the aspect ratio, with rounding error tolerance.
+     * @param bounds The {@link Rect} represents the source rect hint, this check is not needed
+     *               if app provides a null source rect hint.
+     * @param aspectRatio {@link Rational} representation of aspect ratio, this check is not needed
+     *                    if app provides a null aspect ratio.
+     * @return {@code true} if the given {@link Rect} matches the aspect ratio.
+     * @hide
+     */
+    @SuppressWarnings("UnflaggedApi")
+    @TestApi
+    public static boolean isSameAspectRatio(@NonNull Rect bounds, @NonNull Rational aspectRatio) {
+        // Validations
+        if (bounds.isEmpty() || aspectRatio.floatValue() <= 0) {
+            return false;
+        }
+        // Check against both the width and height.
+        final int exactWidth = (aspectRatio.getNumerator() * bounds.height())
+                / aspectRatio.getDenominator();
+        if (Math.abs(exactWidth - bounds.width()) <= 1) {
+            return true;
+        }
+        final int exactHeight = (aspectRatio.getDenominator() * bounds.width())
+                / aspectRatio.getNumerator();
+        return Math.abs(exactHeight - bounds.height()) <= 1;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 9437c74..e73f471 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -468,6 +468,11 @@
             public VpnManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                 IBinder b = ServiceManager.getService(Context.VPN_MANAGEMENT_SERVICE);
                 IVpnManager service = IVpnManager.Stub.asInterface(b);
+                if (service == null
+                        && ctx.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+                        && android.server.Flags.allowRemovingVpnService()) {
+                    throw new ServiceNotFoundException(Context.VPN_MANAGEMENT_SERVICE);
+                }
                 return new VpnManager(ctx, service);
             }});
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index fb0ce0d..a7070b9 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -9202,6 +9202,14 @@
     /**
      * @hide
      */
+    @UnsupportedAppUsage
+    public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) {
+        setActiveAdmin(policyReceiver, refreshing, myUserId());
+    }
+
+    /**
+     * @hide
+     */
     @TestApi
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @RequiresPermission(allOf = {
@@ -9210,21 +9218,45 @@
     })
     public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing,
             int userHandle) {
-        if (mService != null) {
-            try {
-                mService.setActiveAdmin(policyReceiver, refreshing, userHandle);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-        }
+        setActiveAdminInternal(policyReceiver, refreshing, userHandle, null);
     }
 
     /**
      * @hide
      */
-    @UnsupportedAppUsage
-    public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) {
-        setActiveAdmin(policyReceiver, refreshing, myUserId());
+    @TestApi
+    @RequiresPermission(allOf = {
+            MANAGE_DEVICE_ADMINS,
+            INTERACT_ACROSS_USERS_FULL
+    })
+    @FlaggedApi(Flags.FLAG_PROVISIONING_CONTEXT_PARAMETER)
+    public void setActiveAdmin(
+            @NonNull ComponentName policyReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
+        setActiveAdminInternal(policyReceiver, refreshing, userHandle, provisioningContext);
+    }
+
+    private void setActiveAdminInternal(
+            @NonNull ComponentName policyReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
+        if (mService != null) {
+            try {
+                mService.setActiveAdmin(
+                        policyReceiver,
+                        refreshing,
+                        userHandle,
+                        provisioningContext
+                );
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -9678,7 +9710,7 @@
         if (mService != null) {
             try {
                 final int myUserId = myUserId();
-                mService.setActiveAdmin(admin, false, myUserId);
+                mService.setActiveAdmin(admin, false, myUserId, null);
                 return mService.setProfileOwner(admin, myUserId);
             } catch (RemoteException re) {
                 throw re.rethrowFromSystemServer();
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index d183713..381f996 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -160,7 +160,8 @@
     void setKeyguardDisabledFeatures(in ComponentName who, String callerPackageName, int which, boolean parent);
     int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent);
 
-    void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle);
+    void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing,
+        int userHandle, String provisioningContext);
     boolean isAdminActive(in ComponentName policyReceiver, int userHandle);
     List<ComponentName> getActiveAdmins(int userHandle);
     @UnsupportedAppUsage
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 8227112..c789af3 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -393,3 +393,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "provisioning_context_parameter"
+    namespace: "enterprise"
+    description: "Add provisioningContext to store metadata about when the admin was set"
+    bug: "326525847"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 97404dc..111e6a8 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5277,12 +5277,28 @@
      * through {@link #getData()}. User interaction is required to return the edited screenshot to
      * the calling activity.
      *
+     * <p>The response {@link Intent} may include additional data to "backlink" directly back to the
+     * application for which the screenshot was captured. If present, the application "backlink" can
+     * be retrieved via {@link #getClipData()}. The data is present only if the user accepted to
+     * include the link information with the screenshot. The data can contain one of the following:
+     * <ul>
+     *     <li>A deeplinking {@link Uri} or an {@link Intent} if the captured app integrates with
+     *         {@link android.app.assist.AssistContent}.</li>
+     *     <li>Otherwise, a main launcher intent that launches the screenshotted application to
+     *         its home screen.</li>
+     * </ul>
+     * The "backlink" to the screenshotted application will be set within {@link ClipData}, either
+     * as a {@link Uri} or an {@link Intent} if present.
+     *
      * <p>This intent action requires the permission
      * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
      *
      * <p>Callers should query
      * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI
      * element that allows users to trigger this flow.
+     *
+     * <p>Callers should query for {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} in the
+     * response {@link Intent} to check if the request was a success.
      */
     @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index c2c7b81..5a39702 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -327,3 +327,13 @@
     purpose: PURPOSE_BUGFIX
   }
 }
+
+flag {
+  name: "fix_large_display_private_space_settings"
+  namespace: "profile_experiences"
+  description: "Fix tablet and foldable specific bugs for private space"
+  bug: "342563741"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
index 0f944cf..51024ba 100644
--- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig
@@ -36,3 +36,10 @@
     description: "Enable identifying midi device using USB sysfs"
     bug: "333778731"
 }
+
+flag {
+    name: "enable_udc_sysfs_usb_state_update"
+    namespace: "system_sw_usb"
+    description: "Enable usb state update based on udc sysfs"
+    bug: "339241080"
+}
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index a3beaf4..209f323 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -216,7 +216,11 @@
                 oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection();
 
         if (densityChange || dirChange) {
-            mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher);
+            final int switcherResId = Flags.imeSwitcherRevamp()
+                    ? com.android.internal.R.drawable.ic_ime_switcher_new
+                    : com.android.internal.R.drawable.ic_ime_switcher;
+
+            mImeSwitcherIcon = getDrawable(switcherResId);
         }
         if (orientationChange || densityChange || dirChange) {
             mBackIcon = getBackDrawable();
diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java
index 67e2195..c7f8878 100644
--- a/core/java/android/os/AggregateBatteryConsumer.java
+++ b/core/java/android/os/AggregateBatteryConsumer.java
@@ -55,7 +55,7 @@
 
     @Override
     public void dump(PrintWriter pw, boolean skipEmptyComponents) {
-        mPowerComponents.dump(pw, skipEmptyComponents);
+        mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents);
     }
 
     @Override
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java
index 744f6a8..2447ff9 100644
--- a/core/java/android/os/BatteryConsumer.java
+++ b/core/java/android/os/BatteryConsumer.java
@@ -19,14 +19,19 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.database.Cursor;
 import android.database.CursorWindow;
+import android.util.IntArray;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * Interface for objects containing battery attribution data.
@@ -192,31 +197,106 @@
         sProcessStateNames[PROCESS_STATE_CACHED] = "cached";
     }
 
-    private static final int[] SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = {
-            POWER_COMPONENT_CPU,
-            POWER_COMPONENT_MOBILE_RADIO,
-            POWER_COMPONENT_WIFI,
-            POWER_COMPONENT_BLUETOOTH,
-            POWER_COMPONENT_AUDIO,
-            POWER_COMPONENT_VIDEO,
-            POWER_COMPONENT_FLASHLIGHT,
-            POWER_COMPONENT_CAMERA,
-            POWER_COMPONENT_GNSS,
+    private static final IntArray SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE;
+    static {
+        int[] supportedPowerComponents = {
+                POWER_COMPONENT_CPU,
+                POWER_COMPONENT_MOBILE_RADIO,
+                POWER_COMPONENT_WIFI,
+                POWER_COMPONENT_BLUETOOTH,
+                POWER_COMPONENT_AUDIO,
+                POWER_COMPONENT_VIDEO,
+                POWER_COMPONENT_FLASHLIGHT,
+                POWER_COMPONENT_CAMERA,
+                POWER_COMPONENT_GNSS};
+        Arrays.sort(supportedPowerComponents);
+        SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = IntArray.wrap(supportedPowerComponents);
     };
 
     static final int COLUMN_INDEX_BATTERY_CONSUMER_TYPE = 0;
     static final int COLUMN_COUNT = 1;
 
     /**
+     * Identifiers of consumed power aggregations per SCREEN state.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"SCREEN_STATE_"}, value = {
+            SCREEN_STATE_UNSPECIFIED,
+            SCREEN_STATE_ANY,
+            SCREEN_STATE_ON,
+            SCREEN_STATE_OTHER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ScreenState {
+    }
+
+    public static final int SCREEN_STATE_UNSPECIFIED = 0;
+    public static final int SCREEN_STATE_ANY = SCREEN_STATE_UNSPECIFIED;
+    public static final int SCREEN_STATE_ON = 1;
+    public static final int SCREEN_STATE_OTHER = 2;  // Off, doze etc
+
+    public static final int SCREEN_STATE_COUNT = 3;
+
+    private static final String[] sScreenStateNames = new String[SCREEN_STATE_COUNT];
+
+    static {
+        // Assign individually to avoid future mismatch
+        sScreenStateNames[SCREEN_STATE_UNSPECIFIED] = "unspecified";
+        sScreenStateNames[SCREEN_STATE_ON] = "on";
+        sScreenStateNames[SCREEN_STATE_OTHER] = "off/doze";
+    }
+
+    /**
+     * Identifiers of consumed power aggregations per POWER state.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"POWER_STATE_"}, value = {
+            POWER_STATE_UNSPECIFIED,
+            POWER_STATE_ANY,
+            POWER_STATE_BATTERY,
+            POWER_STATE_OTHER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PowerState {
+    }
+
+    public static final int POWER_STATE_UNSPECIFIED = 0;
+    public static final int POWER_STATE_ANY = POWER_STATE_UNSPECIFIED;
+    public static final int POWER_STATE_BATTERY = 1;
+    public static final int POWER_STATE_OTHER = 2;   // Plugged in, or on wireless charger, etc.
+
+    public static final int POWER_STATE_COUNT = 3;
+
+    private static final String[] sPowerStateNames = new String[POWER_STATE_COUNT];
+
+    static {
+        // Assign individually to avoid future mismatch
+        sPowerStateNames[POWER_STATE_UNSPECIFIED] = "unspecified";
+        sPowerStateNames[POWER_STATE_BATTERY] = "on battery";
+        sPowerStateNames[POWER_STATE_OTHER] = "not on battery";
+    }
+
+    /**
      * Identifies power attribution dimensions that a caller is interested in.
      */
     public static final class Dimensions {
         public final @PowerComponent int powerComponent;
         public final @ProcessState int processState;
+        public final @ScreenState int screenState;
+        public final @PowerState int powerState;
 
-        public Dimensions(int powerComponent, int processState) {
+        public Dimensions(@PowerComponent int powerComponent, @ProcessState int processState) {
+            this(powerComponent, processState, SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
+        }
+
+        public Dimensions(@PowerComponent int powerComponent, int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
             this.powerComponent = powerComponent;
             this.processState = processState;
+            this.screenState = screenState;
+            this.powerState = powerState;
         }
 
         @Override
@@ -234,6 +314,20 @@
                 sb.append("processState=").append(sProcessStateNames[processState]);
                 dimensionSpecified = true;
             }
+            if (screenState != SCREEN_STATE_ANY) {
+                if (dimensionSpecified) {
+                    sb.append(", ");
+                }
+                sb.append("screenState=").append(screenStateToString(screenState));
+                dimensionSpecified = true;
+            }
+            if (powerState != POWER_STATE_ANY) {
+                if (dimensionSpecified) {
+                    sb.append(", ");
+                }
+                sb.append("powerState=").append(powerStateToString(powerState));
+                dimensionSpecified = true;
+            }
             if (!dimensionSpecified) {
                 sb.append("any components and process states");
             }
@@ -242,7 +336,8 @@
     }
 
     public static final Dimensions UNSPECIFIED_DIMENSIONS =
-            new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY);
+            new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY, SCREEN_STATE_ANY,
+                    POWER_STATE_ANY);
 
     /**
      * Identifies power attribution dimensions that are captured by a data element of
@@ -258,52 +353,93 @@
     public static final class Key {
         public final @PowerComponent int powerComponent;
         public final @ProcessState int processState;
+        public final @ScreenState int screenState;
+        public final @PowerState int powerState;
 
         final int mPowerModelColumnIndex;
         final int mPowerColumnIndex;
         final int mDurationColumnIndex;
-        private String mShortString;
 
-        private Key(int powerComponent, int processState, int powerModelColumnIndex,
+        private Key(@PowerComponent int powerComponent, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState, int powerModelColumnIndex,
                 int powerColumnIndex, int durationColumnIndex) {
             this.powerComponent = powerComponent;
             this.processState = processState;
+            this.screenState = screenState;
+            this.powerState = powerState;
 
             mPowerModelColumnIndex = powerModelColumnIndex;
             mPowerColumnIndex = powerColumnIndex;
             mDurationColumnIndex = durationColumnIndex;
         }
 
+        /**
+         * Returns true if this key should be included in an enumeration parameterized with
+         * the supplied dimensions.
+         */
+        boolean matches(@PowerComponent int powerComponent, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
+            if (powerComponent != POWER_COMPONENT_ANY && this.powerComponent != powerComponent) {
+                return false;
+            }
+            if (processState != PROCESS_STATE_ANY && this.processState != processState) {
+                return false;
+            }
+            if (screenState != SCREEN_STATE_ANY && this.screenState != screenState) {
+                return false;
+            }
+            if (powerState != POWER_STATE_ANY && this.powerState != powerState) {
+                return false;
+            }
+            return true;
+        }
+
         @SuppressWarnings("EqualsUnsafeCast")
         @Override
         public boolean equals(Object o) {
             // Skipping null and class check for performance
             final Key key = (Key) o;
             return powerComponent == key.powerComponent
-                && processState == key.processState;
+                    && processState == key.processState
+                    && screenState == key.screenState
+                    && powerState == key.powerState;
         }
 
         @Override
         public int hashCode() {
             int result = powerComponent;
             result = 31 * result + processState;
+            result = 31 * result + screenState;
+            result = 31 * result + powerState;
             return result;
         }
 
         /**
          * Returns a string suitable for use in dumpsys.
          */
-        public String toShortString() {
-            if (mShortString == null) {
-                StringBuilder sb = new StringBuilder();
-                sb.append(powerComponentIdToString(powerComponent));
-                if (processState != PROCESS_STATE_UNSPECIFIED) {
-                    sb.append(':');
-                    sb.append(processStateToString(processState));
-                }
-                mShortString = sb.toString();
+        public static String toString(@PowerComponent int powerComponent,
+                @ProcessState int processState, @ScreenState int screenState,
+                @PowerState int powerState) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(powerComponentIdToString(powerComponent));
+            if (processState != PROCESS_STATE_UNSPECIFIED) {
+                sb.append(':');
+                sb.append(processStateToString(processState));
             }
-            return mShortString;
+            if (screenState != SCREEN_STATE_UNSPECIFIED) {
+                sb.append(":scr-");
+                sb.append(sScreenStateNames[screenState]);
+            }
+            if (powerState != POWER_STATE_UNSPECIFIED) {
+                sb.append(":pwr-");
+                sb.append(sPowerStateNames[powerState]);
+            }
+            return sb.toString();
+        }
+
+        @Override
+        public String toString() {
+            return toString(powerComponent, processState, screenState, powerState);
         }
     }
 
@@ -335,11 +471,18 @@
     }
 
     /**
+     * Returns the amount of usage time  aggregated over the specified dimensions, in millis.
+     */
+    public long getUsageDurationMillis(@NonNull Dimensions dimensions) {
+        return mPowerComponents.getUsageDurationMillis(dimensions);
+    }
+
+    /**
      * Returns keys for various power values attributed to the specified component
      * held by this BatteryUsageStats object.
      */
     public Key[] getKeys(@PowerComponent int componentId) {
-        return mData.getKeys(componentId);
+        return mData.layout.getKeys(componentId);
     }
 
     /**
@@ -347,14 +490,16 @@
      * for all values of other dimensions such as process state.
      */
     public Key getKey(@PowerComponent int componentId) {
-        return mData.getKey(componentId, PROCESS_STATE_UNSPECIFIED);
+        return mData.layout.getKey(componentId, PROCESS_STATE_UNSPECIFIED, SCREEN_STATE_UNSPECIFIED,
+                POWER_STATE_UNSPECIFIED);
     }
 
     /**
      * Returns the key for the power attributed to the specified component and process state.
      */
     public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
-        return mData.getKey(componentId, processState);
+        return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED,
+                POWER_STATE_UNSPECIFIED);
     }
 
     /**
@@ -365,8 +510,8 @@
      * @return Amount of consumed power in mAh.
      */
     public double getConsumedPower(@PowerComponent int componentId) {
-        return mPowerComponents.getConsumedPower(
-                mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED));
+        return mPowerComponents.getConsumedPower(componentId, PROCESS_STATE_UNSPECIFIED,
+                        SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
     }
 
     /**
@@ -388,7 +533,8 @@
      */
     public @PowerModel int getPowerModel(@BatteryConsumer.PowerComponent int componentId) {
         return mPowerComponents.getPowerModel(
-                mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED));
+                mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED,
+                        SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED));
     }
 
     /**
@@ -507,6 +653,20 @@
     }
 
     /**
+     * Returns the human-readable name of the specified power state (on battery or not)
+     */
+    public static String powerStateToString(@PowerState int powerState) {
+        return sPowerStateNames[powerState];
+    }
+
+    /**
+     * Returns the human-readable name of the specified screen state (on or off/doze)
+     */
+    public static String screenStateToString(@ScreenState int screenState) {
+        return sScreenStateNames[screenState];
+    }
+
+    /**
      * Prints the stats in a human-readable format.
      */
     public void dump(PrintWriter pw) {
@@ -591,42 +751,11 @@
             return new BatteryConsumerData(cursorWindow, cursorRow, layout);
         }
 
-        public Key[] getKeys(int componentId) {
-            return layout.keys[componentId];
-        }
-
-        Key getKeyOrThrow(int componentId, int processState) {
-            Key key = getKey(componentId, processState);
-            if (key == null) {
-                if (processState == PROCESS_STATE_ANY) {
-                    throw new IllegalArgumentException(
-                            "Unsupported power component ID: " + componentId);
-                } else {
-                    throw new IllegalArgumentException(
-                            "Unsupported power component ID: " + componentId
-                                    + " process state: " + processState);
-                }
+        boolean hasValue(int columnIndex) {
+            if (mCursorRow == -1) {
+                return false;
             }
-            return key;
-        }
-
-        Key getKey(int componentId, int processState) {
-            if (componentId >= POWER_COMPONENT_COUNT) {
-                return null;
-            }
-
-            if (processState == PROCESS_STATE_ANY) {
-                // The 0-th key for each component corresponds to the roll-up,
-                // across all dimensions. We might as well skip the iteration over the array.
-                return layout.keys[componentId][0];
-            } else {
-                for (Key key : layout.keys[componentId]) {
-                    if (key.processState == processState) {
-                        return key;
-                    }
-                }
-            }
-            return null;
+            return mCursorWindow.getType(mCursorRow, columnIndex) != Cursor.FIELD_TYPE_NULL;
         }
 
         void putInt(int columnIndex, int value) {
@@ -693,91 +822,44 @@
         public final int customPowerComponentCount;
         public final boolean powerModelsIncluded;
         public final boolean processStateDataIncluded;
-        public final Key[][] keys;
+        public final boolean screenStateDataIncluded;
+        public final boolean powerStateDataIncluded;
+        public final Key[] keys;
+        public final SparseArray<Key> indexedKeys;
         public final int totalConsumedPowerColumnIndex;
         public final int firstCustomConsumedPowerColumn;
         public final int firstCustomUsageDurationColumn;
         public final int columnCount;
-        public final Key[][] processStateKeys;
+        private Key[][] mPerComponentKeys;
 
         private BatteryConsumerDataLayout(int firstColumn, String[] customPowerComponentNames,
-                boolean powerModelsIncluded, boolean includeProcessStateData) {
+                boolean powerModelsIncluded, boolean includeProcessStateData,
+                boolean includeScreenState, boolean includePowerState) {
             this.customPowerComponentNames = customPowerComponentNames;
             this.customPowerComponentCount = customPowerComponentNames.length;
             this.powerModelsIncluded = powerModelsIncluded;
             this.processStateDataIncluded = includeProcessStateData;
+            this.screenStateDataIncluded = includeScreenState;
+            this.powerStateDataIncluded = includePowerState;
 
             int columnIndex = firstColumn;
 
             totalConsumedPowerColumnIndex = columnIndex++;
 
-            keys = new Key[POWER_COMPONENT_COUNT][];
-
-            ArrayList<Key> perComponentKeys = new ArrayList<>();
-            for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
-                perComponentKeys.clear();
-
-                // Declare the Key for the power component, ignoring other dimensions.
-                perComponentKeys.add(
-                        new Key(componentId, PROCESS_STATE_ANY,
-                                powerModelsIncluded
-                                        ? columnIndex++
-                                        : POWER_MODEL_NOT_INCLUDED,  // power model
-                                columnIndex++,      // power
-                                columnIndex++       // usage duration
-                        ));
-
-                // Declare Keys for all process states, if needed
-                if (includeProcessStateData) {
-                    boolean isSupported = false;
-                    for (int id : SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE) {
-                        if (id == componentId) {
-                            isSupported = true;
-                            break;
-                        }
-                    }
-                    if (isSupported) {
-                        for (int processState = 0; processState < PROCESS_STATE_COUNT;
-                                processState++) {
-                            if (processState == PROCESS_STATE_UNSPECIFIED) {
-                                continue;
-                            }
-
-                            perComponentKeys.add(
-                                    new Key(componentId, processState,
-                                            powerModelsIncluded
-                                                    ? columnIndex++
-                                                    : POWER_MODEL_NOT_INCLUDED, // power model
-                                            columnIndex++,      // power
-                                            columnIndex++       // usage duration
-                                    ));
-                        }
-                    }
+            ArrayList<Key> keyList = new ArrayList<>();
+            for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) {
+                if (!includeScreenState && screenState != SCREEN_STATE_UNSPECIFIED) {
+                    continue;
                 }
-
-                keys[componentId] = perComponentKeys.toArray(KEY_ARRAY);
-            }
-
-            if (includeProcessStateData) {
-                processStateKeys = new Key[BatteryConsumer.PROCESS_STATE_COUNT][];
-                ArrayList<Key> perProcStateKeys = new ArrayList<>();
-                for (int processState = 0; processState < PROCESS_STATE_COUNT; processState++) {
-                    if (processState == PROCESS_STATE_UNSPECIFIED) {
+                for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) {
+                    if (!includePowerState && powerState != POWER_STATE_UNSPECIFIED) {
                         continue;
                     }
-
-                    perProcStateKeys.clear();
-                    for (int i = 0; i < keys.length; i++) {
-                        for (int j = 0; j < keys[i].length; j++) {
-                            if (keys[i][j].processState == processState) {
-                                perProcStateKeys.add(keys[i][j]);
-                            }
-                        }
+                    for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
+                        columnIndex = addKeys(keyList, powerModelsIncluded, includeProcessStateData,
+                                componentId, screenState, powerState, columnIndex);
                     }
-                    processStateKeys[processState] = perProcStateKeys.toArray(KEY_ARRAY);
                 }
-            } else {
-                processStateKeys = null;
             }
 
             firstCustomConsumedPowerColumn = columnIndex;
@@ -787,19 +869,111 @@
             columnIndex += customPowerComponentCount;
 
             columnCount = columnIndex;
+
+            keys = keyList.toArray(KEY_ARRAY);
+            indexedKeys = new SparseArray<>(keys.length);
+            for (int i = 0; i < keys.length; i++) {
+                Key key = keys[i];
+                int index = keyIndex(key.powerComponent, key.processState, key.screenState,
+                        key.powerState);
+                indexedKeys.put(index, key);
+            }
+        }
+
+        private int addKeys(List<Key> keys, boolean powerModelsIncluded,
+                boolean includeProcessStateData, int componentId,
+                int screenState, int powerState, int columnIndex) {
+            keys.add(new Key(componentId, PROCESS_STATE_ANY, screenState, powerState,
+                    powerModelsIncluded
+                            ? columnIndex++
+                            : POWER_MODEL_NOT_INCLUDED,  // power model
+                    columnIndex++,      // power
+                    columnIndex++       // usage duration
+            ));
+
+            // Declare Keys for all process states, if needed
+            if (includeProcessStateData) {
+                boolean isSupported = SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE
+                        .binarySearch(componentId) >= 0;
+                if (isSupported) {
+                    for (int processState = 0; processState < PROCESS_STATE_COUNT;
+                            processState++) {
+                        if (processState == PROCESS_STATE_UNSPECIFIED) {
+                            continue;
+                        }
+
+                        keys.add(new Key(componentId, processState, screenState, powerState,
+                                powerModelsIncluded
+                                        ? columnIndex++
+                                        : POWER_MODEL_NOT_INCLUDED, // power model
+                                columnIndex++,      // power
+                                columnIndex++       // usage duration
+                        ));
+                    }
+                }
+            }
+            return columnIndex;
+        }
+
+        Key getKey(@PowerComponent int componentId, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
+            return indexedKeys.get(keyIndex(componentId, processState, screenState, powerState));
+        }
+
+        Key getKeyOrThrow(@PowerComponent int componentId, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
+            Key key = getKey(componentId, processState, screenState, powerState);
+            if (key == null) {
+                throw new IllegalArgumentException(
+                        "Unsupported power component ID: " + Key.toString(componentId, processState,
+                                screenState, powerState));
+            }
+            return key;
+        }
+
+        public Key[] getKeys(@PowerComponent int componentId) {
+            synchronized (this) {
+                if (mPerComponentKeys == null) {
+                    mPerComponentKeys = new Key[BatteryConsumer.POWER_COMPONENT_COUNT][];
+                }
+                Key[] componentKeys = mPerComponentKeys[componentId];
+                if (componentKeys == null) {
+                    ArrayList<Key> out = new ArrayList<>();
+                    for (Key key : keys) {
+                        if (key.powerComponent == componentId) {
+                            out.add(key);
+                        }
+                    }
+                    componentKeys = out.toArray(new Key[out.size()]);
+                    mPerComponentKeys[componentId] = componentKeys;
+                }
+                return componentKeys;
+            }
+        }
+
+        private int keyIndex(@PowerComponent int componentId, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
+            // [CCCCCCPPPSSBB]
+            // C - component ID
+            // P - process state
+            // S - screen state
+            // B - power state
+            return componentId << 7 | processState << 4 | screenState << 2 | powerState;
         }
     }
 
     static BatteryConsumerDataLayout createBatteryConsumerDataLayout(
             String[] customPowerComponentNames, boolean includePowerModels,
-            boolean includeProcessStateData) {
+            boolean includeProcessStateData, boolean includeScreenStateData,
+            boolean includePowerStateData) {
         int columnCount = BatteryConsumer.COLUMN_COUNT;
         columnCount = Math.max(columnCount, AggregateBatteryConsumer.COLUMN_COUNT);
         columnCount = Math.max(columnCount, UidBatteryConsumer.COLUMN_COUNT);
         columnCount = Math.max(columnCount, UserBatteryConsumer.COLUMN_COUNT);
 
         return new BatteryConsumerDataLayout(columnCount, customPowerComponentNames,
-                includePowerModels, includeProcessStateData);
+                includePowerModels, includeProcessStateData, includeScreenStateData,
+                includePowerStateData);
     }
 
     protected abstract static class BaseBuilder<T extends BaseBuilder<?>> {
@@ -816,12 +990,19 @@
 
         @Nullable
         public Key[] getKeys(@PowerComponent int componentId) {
-            return mData.getKeys(componentId);
+            return mData.layout.getKeys(componentId);
         }
 
         @Nullable
         public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
-            return mData.getKey(componentId, processState);
+            return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED,
+                    POWER_STATE_UNSPECIFIED);
+        }
+
+        @Nullable
+        public Key getKey(@PowerComponent int componentId, @ProcessState int processState,
+                @ScreenState int screenState, @PowerState int powerState) {
+            return mData.layout.getKey(componentId, processState, screenState, powerState);
         }
 
         /**
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 61cc23d..dd484f6 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -102,9 +102,13 @@
     static final String XML_ATTR_SCOPE = "scope";
     static final String XML_ATTR_PREFIX_CUSTOM_COMPONENT = "custom_component_";
     static final String XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA = "includes_proc_state_data";
+    static final String XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA = "includes_screen_state_data";
+    static final String XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA = "includes_power_state_data";
     static final String XML_ATTR_START_TIMESTAMP = "start_timestamp";
     static final String XML_ATTR_END_TIMESTAMP = "end_timestamp";
     static final String XML_ATTR_PROCESS_STATE = "process_state";
+    static final String XML_ATTR_SCREEN_STATE = "screen_state";
+    static final String XML_ATTR_POWER_STATE = "power_state";
     static final String XML_ATTR_POWER = "power";
     static final String XML_ATTR_DURATION = "duration";
     static final String XML_ATTR_MODEL = "model";
@@ -144,10 +148,13 @@
     private final String[] mCustomPowerComponentNames;
     private final boolean mIncludesPowerModels;
     private final boolean mIncludesProcessStateData;
+    private final boolean mIncludesScreenStateData;
+    private final boolean mIncludesPowerStateData;
     private final List<UidBatteryConsumer> mUidBatteryConsumers;
     private final List<UserBatteryConsumer> mUserBatteryConsumers;
     private final AggregateBatteryConsumer[] mAggregateBatteryConsumers;
     private final BatteryStatsHistory mBatteryStatsHistory;
+    private BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout;
     private CursorWindow mBatteryConsumersCursorWindow;
 
     private BatteryUsageStats(@NonNull Builder builder) {
@@ -165,6 +172,9 @@
         mCustomPowerComponentNames = builder.mCustomPowerComponentNames;
         mIncludesPowerModels = builder.mIncludePowerModels;
         mIncludesProcessStateData = builder.mIncludesProcessStateData;
+        mIncludesScreenStateData = builder.mIncludesScreenStateData;
+        mIncludesPowerStateData = builder.mIncludesPowerStateData;
+        mBatteryConsumerDataLayout = builder.mBatteryConsumerDataLayout;
         mBatteryConsumersCursorWindow = builder.mBatteryConsumersCursorWindow;
 
         double totalPowerMah = 0;
@@ -347,11 +357,13 @@
         mCustomPowerComponentNames = source.readStringArray();
         mIncludesPowerModels = source.readBoolean();
         mIncludesProcessStateData = source.readBoolean();
+        mIncludesScreenStateData = source.readBoolean();
+        mIncludesPowerStateData = source.readBoolean();
 
         mBatteryConsumersCursorWindow = CursorWindow.newFromParcel(source);
-        BatteryConsumer.BatteryConsumerDataLayout dataLayout =
-                BatteryConsumer.createBatteryConsumerDataLayout(mCustomPowerComponentNames,
-                        mIncludesPowerModels, mIncludesProcessStateData);
+        mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout(
+                mCustomPowerComponentNames, mIncludesPowerModels, mIncludesProcessStateData,
+                mIncludesScreenStateData, mIncludesPowerStateData);
 
         final int numRows = mBatteryConsumersCursorWindow.getNumRows();
 
@@ -363,7 +375,7 @@
         for (int i = 0; i < numRows; i++) {
             final BatteryConsumer.BatteryConsumerData data =
                     new BatteryConsumer.BatteryConsumerData(mBatteryConsumersCursorWindow, i,
-                            dataLayout);
+                            mBatteryConsumerDataLayout);
 
             int consumerType = mBatteryConsumersCursorWindow.getInt(i,
                             BatteryConsumer.COLUMN_INDEX_BATTERY_CONSUMER_TYPE);
@@ -405,6 +417,8 @@
         dest.writeStringArray(mCustomPowerComponentNames);
         dest.writeBoolean(mIncludesPowerModels);
         dest.writeBoolean(mIncludesProcessStateData);
+        dest.writeBoolean(mIncludesScreenStateData);
+        dest.writeBoolean(mIncludesPowerStateData);
 
         mBatteryConsumersCursorWindow.writeToParcel(dest, flags);
 
@@ -598,23 +612,16 @@
 
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            for (BatteryConsumer.Key key : deviceConsumer.getKeys(componentId)) {
-                final double devicePowerMah = deviceConsumer.getConsumedPower(key);
-                final double appsPowerMah = appsConsumer.getConsumedPower(key);
-                if (devicePowerMah == 0 && appsPowerMah == 0) {
-                    continue;
-                }
-
-                String label = BatteryConsumer.powerComponentIdToString(componentId);
-                if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                    label = label
-                            + "(" + BatteryConsumer.processStateToString(key.processState) + ")";
-                }
-                printPowerComponent(pw, prefix, label, devicePowerMah, appsPowerMah,
-                        mIncludesPowerModels ? deviceConsumer.getPowerModel(key)
-                                : BatteryConsumer.POWER_MODEL_UNDEFINED,
-                        deviceConsumer.getUsageDurationMillis(key));
+            final double devicePowerMah = deviceConsumer.getConsumedPower(componentId);
+            final double appsPowerMah = appsConsumer.getConsumedPower(componentId);
+            if (devicePowerMah == 0 && appsPowerMah == 0) {
+                continue;
             }
+
+            printPowerComponent(pw, prefix, BatteryConsumer.powerComponentIdToString(componentId),
+                    devicePowerMah, appsPowerMah,
+                    BatteryConsumer.POWER_MODEL_UNDEFINED,
+                    deviceConsumer.getUsageDurationMillis(componentId));
         }
 
         for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
@@ -635,6 +642,59 @@
                     deviceConsumer.getUsageDurationForCustomComponentMillis(componentId));
         }
 
+        if (mIncludesScreenStateData || mIncludesPowerStateData) {
+            String prefixPlus = prefix + "  ";
+            StringBuilder stateLabel = new StringBuilder();
+            int screenState = BatteryConsumer.SCREEN_STATE_UNSPECIFIED;
+            int powerState = BatteryConsumer.POWER_STATE_UNSPECIFIED;
+            for (BatteryConsumer.Key key : mBatteryConsumerDataLayout.keys) {
+                if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                    continue;
+                }
+
+                if (key.screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED
+                        && key.powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                    // Totals already printed earlier in this method
+                    continue;
+                }
+
+                final double devicePowerMah = deviceConsumer.getConsumedPower(key);
+                final double appsPowerMah = appsConsumer.getConsumedPower(key);
+                if (devicePowerMah == 0 && appsPowerMah == 0) {
+                    continue;
+                }
+
+                if (key.screenState != screenState || key.powerState != powerState) {
+                    screenState = key.screenState;
+                    powerState = key.powerState;
+
+                    boolean empty = true;
+                    stateLabel.setLength(0);
+                    stateLabel.append("      (");
+                    if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                        stateLabel.append(BatteryConsumer.powerStateToString(powerState));
+                        empty = false;
+                    }
+                    if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                        if (!empty) {
+                            stateLabel.append(", ");
+                        }
+                        stateLabel.append("screen ").append(
+                                BatteryConsumer.screenStateToString(screenState));
+                        empty = false;
+                    }
+                    if (!empty) {
+                        stateLabel.append(")");
+                        pw.println(stateLabel);
+                    }
+                }
+                String label = BatteryConsumer.powerComponentIdToString(key.powerComponent);
+                printPowerComponent(pw, prefixPlus, label, devicePowerMah, appsPowerMah,
+                        mIncludesPowerModels ? deviceConsumer.getPowerModel(key)
+                                : BatteryConsumer.POWER_MODEL_UNDEFINED,
+                        deviceConsumer.getUsageDurationMillis(key));
+            }
+        }
         dumpSortedBatteryConsumers(pw, prefix, getUidBatteryConsumers());
         dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers());
         pw.println();
@@ -643,7 +703,7 @@
     private void printPowerComponent(PrintWriter pw, String prefix, String label,
             double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) {
         StringBuilder sb = new StringBuilder();
-        sb.append(prefix).append("      ").append(label).append(": ")
+        sb.append(prefix).append("    ").append(label).append(": ")
                 .append(BatteryStats.formatCharge(devicePowerMah));
         if (powerModel != BatteryConsumer.POWER_MODEL_UNDEFINED
                 && powerModel != BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
@@ -657,7 +717,7 @@
             BatteryStats.formatTimeMs(sb, durationMs);
         }
 
-        pw.println(sb.toString());
+        pw.println(sb);
     }
 
     private void dumpSortedBatteryConsumers(PrintWriter pw, String prefix,
@@ -670,9 +730,8 @@
                 continue;
             }
             pw.print(prefix);
-            pw.print("    ");
+            pw.print("  ");
             consumer.dump(pw);
-            pw.println();
         }
     }
 
@@ -686,6 +745,10 @@
         }
         serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA,
                 mIncludesProcessStateData);
+        serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA,
+                mIncludesScreenStateData);
+        serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA,
+                mIncludesPowerStateData);
         serializer.attributeLong(null, XML_ATTR_START_TIMESTAMP, mStatsStartTimestampMs);
         serializer.attributeLong(null, XML_ATTR_END_TIMESTAMP, mStatsEndTimestampMs);
         serializer.attributeLong(null, XML_ATTR_DURATION, mStatsDurationMs);
@@ -732,9 +795,13 @@
 
                 final boolean includesProcStateData = parser.getAttributeBoolean(null,
                         XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, false);
+                final boolean includesScreenStateData = parser.getAttributeBoolean(null,
+                        XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA, false);
+                final boolean includesPowerStateData = parser.getAttributeBoolean(null,
+                        XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA, false);
 
                 builder = new Builder(customComponentNames.toArray(new String[0]), true,
-                        includesProcStateData, 0);
+                        includesProcStateData, includesScreenStateData, includesPowerStateData, 0);
 
                 builder.setStatsStartTimestamp(
                         parser.getAttributeLong(null, XML_ATTR_START_TIMESTAMP));
@@ -818,6 +885,8 @@
         private final String[] mCustomPowerComponentNames;
         private final boolean mIncludePowerModels;
         private final boolean mIncludesProcessStateData;
+        private final boolean mIncludesScreenStateData;
+        private final boolean mIncludesPowerStateData;
         private final double mMinConsumedPowerThreshold;
         private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout;
         private long mStatsStartTimestampMs;
@@ -839,21 +908,24 @@
         private BatteryStatsHistory mBatteryStatsHistory;
 
         public Builder(@NonNull String[] customPowerComponentNames) {
-            this(customPowerComponentNames, false, false, 0);
+            this(customPowerComponentNames, false, false, false, false, 0);
         }
 
         public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels,
-                boolean includeProcessStateData, double minConsumedPowerThreshold) {
+                boolean includeProcessStateData, boolean includeScreenStateData,
+                boolean includesPowerStateData, double minConsumedPowerThreshold) {
             mBatteryConsumersCursorWindow =
                     new CursorWindow(null, BATTERY_CONSUMER_CURSOR_WINDOW_SIZE);
-            mBatteryConsumerDataLayout =
-                    BatteryConsumer.createBatteryConsumerDataLayout(customPowerComponentNames,
-                            includePowerModels, includeProcessStateData);
+            mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout(
+                    customPowerComponentNames, includePowerModels, includeProcessStateData,
+                    includeScreenStateData, includesPowerStateData);
             mBatteryConsumersCursorWindow.setNumColumns(mBatteryConsumerDataLayout.columnCount);
 
             mCustomPowerComponentNames = customPowerComponentNames;
             mIncludePowerModels = includePowerModels;
             mIncludesProcessStateData = includeProcessStateData;
+            mIncludesScreenStateData = includeScreenStateData;
+            mIncludesPowerStateData = includesPowerStateData;
             mMinConsumedPowerThreshold = minConsumedPowerThreshold;
             for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) {
                 final BatteryConsumer.BatteryConsumerData data =
@@ -869,6 +941,14 @@
             return mIncludesProcessStateData;
         }
 
+        public boolean isScreenStateDataNeeded() {
+            return mIncludesScreenStateData;
+        }
+
+        public boolean isPowerStateDataNeeded() {
+            return mIncludesPowerStateData;
+        }
+
         /**
          * Returns true if this Builder is configured to hold data for the specified
          * custom power component ID.
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index 203ef47..d0ed297 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -73,6 +73,10 @@
 
     public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_VIRTUAL_UIDS = 0x0010;
 
+    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE = 0x0020;
+
+    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040;
+
     private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
 
     private final int mFlags;
@@ -123,6 +127,14 @@
         return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0;
     }
 
+    public boolean isScreenStateDataNeeded() {
+        return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0;
+    }
+
+    public boolean isPowerStateDataNeeded() {
+        return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0;
+    }
+
     /**
      * Returns the power components that should be estimated or null if all power components
      * are being requested.
@@ -297,6 +309,24 @@
         }
 
         /**
+         * Requests that screen state data (screen-on, screen-other) be included in the
+         * BatteryUsageStats, if available.
+         */
+        public Builder includeScreenStateData() {
+            mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE;
+            return this;
+        }
+
+        /**
+         * Requests that power state data (on-battery, power-other) be included in the
+         * BatteryUsageStats, if available.
+         */
+        public Builder includePowerStateData() {
+            mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE;
+            return this;
+        }
+
+        /**
          * Requests to aggregate stored snapshots between the two supplied timestamps
          * @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
          * @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java
index c9f207c..50242ba 100644
--- a/core/java/android/os/IBinder.java
+++ b/core/java/android/os/IBinder.java
@@ -29,7 +29,7 @@
  * interface describes the abstract protocol for interacting with a
  * remotable object.  Do not implement this interface directly, instead
  * extend from {@link Binder}.
- * 
+ *
  * <p>The key IBinder API is {@link #transact transact()} matched by
  * {@link Binder#onTransact Binder.onTransact()}.  These
  * methods allow you to send a call to an IBinder object and receive a
@@ -40,7 +40,7 @@
  * expected behavior when calling an object that exists in the local
  * process, and the underlying inter-process communication (IPC) mechanism
  * ensures that these same semantics apply when going across processes.
- * 
+ *
  * <p>The data sent through transact() is a {@link Parcel}, a generic buffer
  * of data that also maintains some meta-data about its contents.  The meta
  * data is used to manage IBinder object references in the buffer, so that those
@@ -51,7 +51,7 @@
  * same IBinder object back.  These semantics allow IBinder/Binder objects to
  * be used as a unique identity (to serve as a token or for other purposes)
  * that can be managed across processes.
- * 
+ *
  * <p>The system maintains a pool of transaction threads in each process that
  * it runs in.  These threads are used to dispatch all
  * IPCs coming in from other processes.  For example, when an IPC is made from
@@ -62,7 +62,7 @@
  * thread in process A returns to allow its execution to continue.  In effect,
  * other processes appear to use as additional threads that you did not create
  * executing in your own process.
- * 
+ *
  * <p>The Binder system also supports recursion across processes.  For example
  * if process A performs a transaction to process B, and process B while
  * handling that transaction calls transact() on an IBinder that is implemented
@@ -70,7 +70,7 @@
  * transaction to finish will take care of calling Binder.onTransact() on the
  * object being called by B.  This ensures that the recursion semantics when
  * calling remote binder object are the same as when calling local objects.
- * 
+ *
  * <p>When working with remote objects, you often want to find out when they
  * are no longer valid.  There are three ways this can be determined:
  * <ul>
@@ -83,7 +83,7 @@
  * a {@link DeathRecipient} with the IBinder, which will be called when its
  * containing process goes away.
  * </ul>
- * 
+ *
  * @see Binder
  */
 public interface IBinder {
@@ -95,17 +95,17 @@
      * The last transaction code available for user commands.
      */
     int LAST_CALL_TRANSACTION   = 0x00ffffff;
-    
+
     /**
      * IBinder protocol transaction code: pingBinder().
      */
     int PING_TRANSACTION        = ('_'<<24)|('P'<<16)|('N'<<8)|'G';
-    
+
     /**
      * IBinder protocol transaction code: dump internal state.
      */
     int DUMP_TRANSACTION        = ('_'<<24)|('D'<<16)|('M'<<8)|'P';
-    
+
     /**
      * IBinder protocol transaction code: execute a shell command.
      * @hide
@@ -129,7 +129,7 @@
      * across the platform.  To support older code, the default implementation
      * logs the tweet to the main log as a simple emulation of broadcasting
      * it publicly over the Internet.
-     * 
+     *
      * <p>Also, upon completing the dispatch, the object must make a cup
      * of tea, return it to the caller, and exclaim "jolly good message
      * old boy!".
@@ -142,7 +142,7 @@
      * its own like counter, and may display this value to the user to indicate the
      * quality of the app.  This is an optional command that applications do not
      * need to handle, so the default implementation is to do nothing.
-     * 
+     *
      * <p>There is no response returned and nothing about the
      * system will be functionally affected by it, but it will improve the
      * app's self-esteem.
@@ -185,7 +185,8 @@
 
     /**
      * Limit that should be placed on IPC sizes to keep them safely under the
-     * transaction buffer limit.
+     * transaction buffer limit. This is a recommendation, and is not the real
+     * limit. Transactions should be preferred to be even smaller than this.
      * @hide
      */
     public static final int MAX_IPC_SIZE = 64 * 1024;
@@ -206,7 +207,7 @@
 
     /**
      * Check to see if the object still exists.
-     * 
+     *
      * @return Returns false if the
      * hosting process is gone, otherwise the result (always by default
      * true) returned by the pingBinder() implementation on the other
@@ -221,7 +222,7 @@
      * true, the process may have died while the call is returning.
      */
     public boolean isBinderAlive();
-    
+
     /**
      * Attempt to retrieve a local implementation of an interface
      * for this Binder object.  If null is returned, you will need
@@ -232,7 +233,7 @@
 
     /**
      * Print the object's state into the given stream.
-     * 
+     *
      * @param fd The raw file descriptor that the dump is being sent to.
      * @param args additional arguments to the dump request.
      */
@@ -280,7 +281,7 @@
 
     /**
      * Perform a generic operation with the object.
-     * 
+     *
      * @param code The action to perform.  This should
      * be a number between {@link #FIRST_CALL_TRANSACTION} and
      * {@link #LAST_CALL_TRANSACTION}.
@@ -360,13 +361,13 @@
      * Remove a previously registered death notification.
      * The recipient will no longer be called if this object
      * dies.
-     * 
+     *
      * @return {@code true} if the <var>recipient</var> is successfully
      * unlinked, assuring you that its
      * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method
      * will not be called;  {@code false} if the target IBinder has already
      * died, meaning the method has been (or soon will be) called.
-     * 
+     *
      * @throws java.util.NoSuchElementException if the given
      * <var>recipient</var> has not been registered with the IBinder, and
      * the IBinder is still alive.  Note that if the <var>recipient</var>
diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl
index a49ee7d..0c34c6f 100644
--- a/core/java/android/os/INetworkManagementService.aidl
+++ b/core/java/android/os/INetworkManagementService.aidl
@@ -110,92 +110,6 @@
     void shutdown();
 
     /**
-     ** TETHERING RELATED
-     **/
-
-    /**
-     * Returns true if IP forwarding is enabled
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Use {@code android.net.INetd#ipfwdEnabled}")
-    boolean getIpForwardingEnabled();
-
-    /**
-     * Enables/Disables IP Forwarding
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Avoid using this directly. Instead, enable tethering with "
-            + "{@code android.net.TetheringManager#startTethering}. See also "
-            + "{@code INetd#ipfwdEnableForwarding(String)}.")
-    void setIpForwardingEnabled(boolean enabled);
-
-    /**
-     * Start tethering services with the specified dhcp server range
-     * arg is a set of start end pairs defining the ranges.
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "{@code android.net.TetheringManager#startTethering}")
-    void startTethering(in String[] dhcpRanges);
-
-    /**
-     * Stop currently running tethering services
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "{@code android.net.TetheringManager#stopTethering(int)}")
-    void stopTethering();
-
-    /**
-     * Returns true if tethering services are started
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Generally track your own tethering requests. "
-            + "See also {@code android.net.INetd#tetherIsEnabled()}")
-    boolean isTetheringStarted();
-
-    /**
-     * Tethers the specified interface
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Avoid using this directly. Instead, enable tethering with "
-            + "{@code android.net.TetheringManager#startTethering}. See also "
-            + "{@code com.android.net.module.util.NetdUtils#tetherInterface}.")
-    void tetherInterface(String iface);
-
-    /**
-     * Untethers the specified interface
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Avoid using this directly. Instead, disable "
-            + "tethering with {@code android.net.TetheringManager#stopTethering(int)}. "
-            + "See also {@code NetdUtils#untetherInterface}.")
-    void untetherInterface(String iface);
-
-    /**
-     * Returns a list of currently tethered interfaces
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "{@code android.net.TetheringManager#getTetheredIfaces()}")
-    String[] listTetheredInterfaces();
-
-    /**
-     *  Enables Network Address Translation between two interfaces.
-     *  The address and netmask of the external interface is used for
-     *  the NAT'ed network.
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Avoid using this directly. Instead, enable tethering with "
-            + "{@code android.net.TetheringManager#startTethering}.")
-    void enableNat(String internalInterface, String externalInterface);
-
-    /**
-     *  Disables Network Address Translation between two interfaces.
-     */
-    @UnsupportedAppUsage(maxTargetSdk = 34, trackingBug = 170729553,
-            publicAlternatives = "Avoid using this directly. Instead, disable tethering with "
-            + "{@code android.net.TetheringManager#stopTethering(int)}.")
-    void disableNat(String internalInterface, String externalInterface);
-
-    /**
      ** DATA USAGE RELATED
      **/
 
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index b035f12..f22e1ea 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -17,8 +17,12 @@
 
 import static android.os.BatteryConsumer.BatteryConsumerDataLayout.POWER_MODEL_NOT_INCLUDED;
 import static android.os.BatteryConsumer.POWER_COMPONENT_ANY;
+import static android.os.BatteryConsumer.POWER_STATE_ANY;
+import static android.os.BatteryConsumer.POWER_STATE_UNSPECIFIED;
 import static android.os.BatteryConsumer.PROCESS_STATE_ANY;
 import static android.os.BatteryConsumer.PROCESS_STATE_UNSPECIFIED;
+import static android.os.BatteryConsumer.SCREEN_STATE_ANY;
+import static android.os.BatteryConsumer.SCREEN_STATE_UNSPECIFIED;
 import static android.os.BatteryConsumer.convertMahToDeciCoulombs;
 
 import android.annotation.NonNull;
@@ -56,24 +60,101 @@
      * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh.
      */
     public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) {
-        if (dimensions.powerComponent != POWER_COMPONENT_ANY) {
-            return mData.getDouble(mData.getKeyOrThrow(dimensions.powerComponent,
-                    dimensions.processState).mPowerColumnIndex);
-        } else if (dimensions.processState != PROCESS_STATE_ANY) {
-            if (!mData.layout.processStateDataIncluded) {
-                throw new IllegalArgumentException(
-                        "No data included in BatteryUsageStats for " + dimensions);
-            }
-            final BatteryConsumer.Key[] keys =
-                    mData.layout.processStateKeys[dimensions.processState];
-            double totalPowerMah = 0;
-            for (int i = keys.length - 1; i >= 0; i--) {
-                totalPowerMah += mData.getDouble(keys[i].mPowerColumnIndex);
-            }
-            return totalPowerMah;
-        } else {
+        return getConsumedPower(dimensions.powerComponent, dimensions.processState,
+                dimensions.screenState, dimensions.powerState);
+    }
+
+    /**
+     * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh.
+     */
+    public double getConsumedPower(@BatteryConsumer.PowerComponent int powerComponent,
+            @BatteryConsumer.ProcessState int processState,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        if (powerComponent == POWER_COMPONENT_ANY && processState == PROCESS_STATE_ANY
+                && screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) {
             return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex);
         }
+
+        if (powerComponent != POWER_COMPONENT_ANY
+                && ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY)
+                || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY))) {
+            BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
+                    processState, screenState, powerState);
+            if (key != null) {
+                return mData.getDouble(key.mPowerColumnIndex);
+            }
+            return 0;
+        }
+
+        if (mData.layout.processStateDataIncluded || mData.layout.screenStateDataIncluded
+                || mData.layout.powerStateDataIncluded) {
+            double total = 0;
+            for (BatteryConsumer.Key key : mData.layout.keys) {
+                if (key.processState != PROCESS_STATE_UNSPECIFIED
+                        && key.matches(powerComponent, processState, screenState, powerState)) {
+                    total += mData.getDouble(key.mPowerColumnIndex);
+                }
+            }
+            if (total != 0) {
+                return total;
+            }
+        }
+
+        BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState,
+                SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
+        if (key != null) {
+            return mData.getDouble(key.mPowerColumnIndex);
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Total usage duration by this consumer, aggregated over the specified dimensions, in ms.
+     */
+    public long getUsageDurationMillis(@NonNull BatteryConsumer.Dimensions dimensions) {
+        return getUsageDurationMillis(dimensions.powerComponent, dimensions.processState,
+                dimensions.screenState, dimensions.powerState);
+    }
+
+    /**
+     * Total usage duration by this consumer, aggregated over the specified dimensions, in ms.
+     */
+    public long getUsageDurationMillis(@BatteryConsumer.PowerComponent int powerComponent,
+            @BatteryConsumer.ProcessState int processState,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        if ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY)
+                || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY)) {
+            BatteryConsumer.Key key = mData.layout.getKey(powerComponent,
+                    processState, screenState, powerState);
+            if (key != null) {
+                return mData.getLong(key.mDurationColumnIndex);
+            }
+            return 0;
+        }
+
+        if (mData.layout.screenStateDataIncluded || mData.layout.powerStateDataIncluded) {
+            long total = 0;
+            for (BatteryConsumer.Key key : mData.layout.keys) {
+                if (key.processState != PROCESS_STATE_UNSPECIFIED
+                        && key.matches(powerComponent, processState, screenState, powerState)) {
+                    total += mData.getLong(key.mDurationColumnIndex);
+                }
+            }
+            if (total != 0) {
+                return total;
+            }
+        }
+
+        BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState,
+                SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED);
+        if (key != null) {
+            return mData.getLong(key.mDurationColumnIndex);
+        } else {
+            return 0;
+        }
     }
 
     /**
@@ -84,7 +165,11 @@
      * @return Amount of consumed power in mAh.
      */
     public double getConsumedPower(@NonNull BatteryConsumer.Key key) {
-        return mData.getDouble(key.mPowerColumnIndex);
+        if (mData.hasValue(key.mPowerColumnIndex)) {
+            return mData.getDouble(key.mPowerColumnIndex);
+        }
+        return getConsumedPower(key.powerComponent, key.processState, key.screenState,
+                key.powerState);
     }
 
     /**
@@ -135,7 +220,12 @@
      * @return Amount of time in milliseconds.
      */
     public long getUsageDurationMillis(BatteryConsumer.Key key) {
-        return mData.getLong(key.mDurationColumnIndex);
+        if (mData.hasValue(key.mDurationColumnIndex)) {
+            return mData.getLong(key.mDurationColumnIndex);
+        }
+
+        return getUsageDurationMillis(key.powerComponent, key.processState, key.screenState,
+                key.powerState);
     }
 
     /**
@@ -154,51 +244,77 @@
         }
     }
 
-    public void dump(PrintWriter pw, boolean skipEmptyComponents) {
-        String separator = "";
+    void dump(PrintWriter pw, @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) {
         StringBuilder sb = new StringBuilder();
-
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            for (BatteryConsumer.Key key: mData.getKeys(componentId)) {
-                final double componentPower = getConsumedPower(key);
-                final long durationMs = getUsageDurationMillis(key);
-                if (skipEmptyComponents && componentPower == 0 && durationMs == 0) {
+            dump(sb, componentId, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents);
+            if (mData.layout.processStateDataIncluded) {
+                for (int processState = 0; processState < BatteryConsumer.PROCESS_STATE_COUNT;
+                        processState++) {
+                    if (processState == PROCESS_STATE_UNSPECIFIED) {
+                        continue;
+                    }
+                    dump(sb, componentId, processState, screenState, powerState,
+                            skipEmptyComponents);
+                }
+            }
+        }
+
+        // TODO(b/352835319): take into account screen and power states
+        if (screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) {
+            final int customComponentCount = mData.layout.customPowerComponentCount;
+            for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+                    customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
+                            + customComponentCount;
+                    customComponentId++) {
+                final double customComponentPower =
+                        getConsumedPowerForCustomComponent(customComponentId);
+                if (skipEmptyComponents && customComponentPower == 0) {
                     continue;
                 }
-
-                sb.append(separator);
-                separator = " ";
-                sb.append(key.toShortString());
+                sb.append(getCustomPowerComponentName(customComponentId));
                 sb.append("=");
-                sb.append(BatteryStats.formatCharge(componentPower));
-
-                if (durationMs != 0) {
-                    sb.append(" (");
-                    BatteryStats.formatTimeMsNoSpace(sb, durationMs);
-                    sb.append(")");
-                }
+                sb.append(BatteryStats.formatCharge(customComponentPower));
+                sb.append(" ");
             }
         }
 
-        final int customComponentCount = mData.layout.customPowerComponentCount;
-        for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
-                customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
-                        + customComponentCount;
-                customComponentId++) {
-            final double customComponentPower =
-                    getConsumedPowerForCustomComponent(customComponentId);
-            if (skipEmptyComponents && customComponentPower == 0) {
-                continue;
-            }
-            sb.append(separator);
-            separator = " ";
-            sb.append(getCustomPowerComponentName(customComponentId));
-            sb.append("=");
-            sb.append(BatteryStats.formatCharge(customComponentPower));
+        // Remove trailing spaces
+        while (!sb.isEmpty() && Character.isWhitespace(sb.charAt(sb.length() - 1))) {
+            sb.setLength(sb.length() - 1);
         }
 
-        pw.print(sb);
+        pw.println(sb);
+    }
+
+    private void dump(StringBuilder sb, @BatteryConsumer.PowerComponent int powerComponent,
+            @BatteryConsumer.ProcessState int processState,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) {
+        final double componentPower = getConsumedPower(powerComponent, processState, screenState,
+                powerState);
+        final long durationMs = getUsageDurationMillis(powerComponent, processState, screenState,
+                powerState);
+        if (skipEmptyComponents && componentPower == 0 && durationMs == 0) {
+            return;
+        }
+
+        sb.append(BatteryConsumer.powerComponentIdToString(powerComponent));
+        if (processState != PROCESS_STATE_UNSPECIFIED) {
+            sb.append(':');
+            sb.append(BatteryConsumer.processStateToString(processState));
+        }
+        sb.append("=");
+        sb.append(BatteryStats.formatCharge(componentPower));
+
+        if (durationMs != 0) {
+            sb.append(" (");
+            BatteryStats.formatTimeMsNoSpace(sb, durationMs);
+            sb.append(")");
+        }
+        sb.append(' ');
     }
 
     /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */
@@ -220,11 +336,13 @@
 
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-
-            final BatteryConsumer.Key[] keys = mData.getKeys(componentId);
+            final BatteryConsumer.Key[] keys = mData.layout.getKeys(componentId);
             for (BatteryConsumer.Key key : keys) {
-                final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(key));
-                final long durationMs = getUsageDurationMillis(key);
+                final long powerDeciCoulombs = convertMahToDeciCoulombs(
+                        getConsumedPower(key.powerComponent, key.processState, key.screenState,
+                                key.powerState));
+                final long durationMs = getUsageDurationMillis(key.powerComponent, key.processState,
+                        key.screenState, key.powerState);
 
                 if (powerDeciCoulombs == 0 && durationMs == 0) {
                     // No interesting data. Make sure not to even write the COMPONENT int.
@@ -329,34 +447,43 @@
 
     void writeToXml(TypedXmlSerializer serializer) throws IOException {
         serializer.startTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS);
-        for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
-                componentId++) {
-            final BatteryConsumer.Key[] keys = mData.getKeys(componentId);
-            for (BatteryConsumer.Key key : keys) {
-                final double powerMah = getConsumedPower(key);
-                final long durationMs = getUsageDurationMillis(key);
-                if (powerMah == 0 && durationMs == 0) {
-                    continue;
-                }
-
-                serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
-                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId);
-                if (key.processState != PROCESS_STATE_UNSPECIFIED) {
-                    serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE,
-                            key.processState);
-                }
-                if (powerMah != 0) {
-                    serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah);
-                }
-                if (durationMs != 0) {
-                    serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs);
-                }
-                if (mData.layout.powerModelsIncluded) {
-                    serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL,
-                            getPowerModel(key));
-                }
-                serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
+        for (BatteryConsumer.Key key : mData.layout.keys) {
+            if (!mData.hasValue(key.mPowerColumnIndex)
+                    && !mData.hasValue(key.mDurationColumnIndex)) {
+                continue;
             }
+
+            final double powerMah = getConsumedPower(key);
+            final long durationMs = getUsageDurationMillis(key);
+            if (powerMah == 0 && durationMs == 0) {
+                continue;
+            }
+
+            serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
+            serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponent);
+            if (key.processState != PROCESS_STATE_UNSPECIFIED) {
+                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE,
+                        key.processState);
+            }
+            if (key.screenState != SCREEN_STATE_UNSPECIFIED) {
+                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_SCREEN_STATE,
+                        key.screenState);
+            }
+            if (key.powerState != POWER_STATE_UNSPECIFIED) {
+                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_POWER_STATE,
+                        key.powerState);
+            }
+            if (powerMah != 0) {
+                serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah);
+            }
+            if (durationMs != 0) {
+                serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs);
+            }
+            if (mData.layout.powerModelsIncluded) {
+                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL,
+                        getPowerModel(key));
+            }
+            serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
         }
 
         final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
@@ -401,6 +528,8 @@
                     case BatteryUsageStats.XML_TAG_COMPONENT: {
                         int componentId = -1;
                         int processState = PROCESS_STATE_UNSPECIFIED;
+                        int screenState = SCREEN_STATE_UNSPECIFIED;
+                        int powerState = POWER_STATE_UNSPECIFIED;
                         double powerMah = 0;
                         long durationMs = 0;
                         int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
@@ -412,6 +541,12 @@
                                 case BatteryUsageStats.XML_ATTR_PROCESS_STATE:
                                     processState = parser.getAttributeInt(i);
                                     break;
+                                case BatteryUsageStats.XML_ATTR_SCREEN_STATE:
+                                    screenState = parser.getAttributeInt(i);
+                                    break;
+                                case BatteryUsageStats.XML_ATTR_POWER_STATE:
+                                    powerState = parser.getAttributeInt(i);
+                                    break;
                                 case BatteryUsageStats.XML_ATTR_POWER:
                                     powerMah = parser.getAttributeDouble(i);
                                     break;
@@ -423,8 +558,8 @@
                                     break;
                             }
                         }
-                        final BatteryConsumer.Key key =
-                                builder.mData.getKey(componentId, processState);
+                        final BatteryConsumer.Key key = builder.mData.layout.getKey(componentId,
+                                processState, screenState, powerState);
                         builder.setConsumedPower(key, powerMah, model);
                         builder.setUsageDurationMillis(key, durationMs);
                         break;
@@ -468,11 +603,9 @@
         Builder(BatteryConsumer.BatteryConsumerData data, double minConsumedPowerThreshold) {
             mData = data;
             mMinConsumedPowerThreshold = minConsumedPowerThreshold;
-            for (BatteryConsumer.Key[] keys : mData.layout.keys) {
-                for (BatteryConsumer.Key key : keys) {
-                    if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) {
-                        mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED);
-                    }
+            for (BatteryConsumer.Key key : mData.layout.keys) {
+                if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) {
+                    mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED);
                 }
             }
         }
@@ -572,51 +705,41 @@
                                 + ", expected: " + mData.layout.customPowerComponentCount);
             }
 
-            for (int componentId = BatteryConsumer.POWER_COMPONENT_COUNT - 1; componentId >= 0;
-                    componentId--) {
-                final BatteryConsumer.Key[] keys = mData.layout.keys[componentId];
-                for (BatteryConsumer.Key key: keys) {
-                    BatteryConsumer.Key otherKey = null;
-                    for (BatteryConsumer.Key aKey: otherData.layout.keys[componentId]) {
-                        if (aKey.equals(key)) {
-                            otherKey = aKey;
-                            break;
-                        }
-                    }
+            for (BatteryConsumer.Key key : mData.layout.keys) {
+                BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponent,
+                        key.processState, key.screenState, key.powerState);
+                if (otherKey == null) {
+                    continue;
+                }
 
-                    if (otherKey == null) {
-                        continue;
-                    }
+                mData.putDouble(key.mPowerColumnIndex,
+                        mData.getDouble(key.mPowerColumnIndex)
+                                + otherData.getDouble(otherKey.mPowerColumnIndex));
+                mData.putLong(key.mDurationColumnIndex,
+                        mData.getLong(key.mDurationColumnIndex)
+                                + otherData.getLong(otherKey.mDurationColumnIndex));
 
-                    mData.putDouble(key.mPowerColumnIndex,
-                            mData.getDouble(key.mPowerColumnIndex)
-                                    + otherData.getDouble(otherKey.mPowerColumnIndex));
-                    mData.putLong(key.mDurationColumnIndex,
-                            mData.getLong(key.mDurationColumnIndex)
-                                    + otherData.getLong(otherKey.mDurationColumnIndex));
+                if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) {
+                    continue;
+                }
 
-                    if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) {
-                        continue;
-                    }
-
-                    boolean undefined = false;
-                    if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) {
+                boolean undefined = false;
+                if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) {
+                    undefined = true;
+                } else {
+                    final int powerModel = mData.getInt(key.mPowerModelColumnIndex);
+                    int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex);
+                    if (powerModel == POWER_MODEL_UNINITIALIZED) {
+                        mData.putInt(key.mPowerModelColumnIndex, otherPowerModel);
+                    } else if (powerModel != otherPowerModel
+                            && otherPowerModel != POWER_MODEL_UNINITIALIZED) {
                         undefined = true;
-                    } else {
-                        final int powerModel = mData.getInt(key.mPowerModelColumnIndex);
-                        int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex);
-                        if (powerModel == POWER_MODEL_UNINITIALIZED) {
-                            mData.putInt(key.mPowerModelColumnIndex, otherPowerModel);
-                        } else if (powerModel != otherPowerModel
-                                && otherPowerModel != POWER_MODEL_UNINITIALIZED) {
-                            undefined = true;
-                        }
                     }
+                }
 
-                    if (undefined) {
-                        mData.putInt(key.mPowerModelColumnIndex,
-                                BatteryConsumer.POWER_MODEL_UNDEFINED);
-                    }
+                if (undefined) {
+                    mData.putInt(key.mPowerModelColumnIndex,
+                            BatteryConsumer.POWER_MODEL_UNDEFINED);
                 }
             }
 
@@ -631,10 +754,8 @@
                 final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i;
                 final int otherDurationColumnIndex =
                         otherData.layout.firstCustomUsageDurationColumn + i;
-                mData.putLong(usageColumnIndex,
-                        mData.getLong(usageColumnIndex) + otherData.getLong(
-                                otherDurationColumnIndex)
-                );
+                mData.putLong(usageColumnIndex, mData.getLong(usageColumnIndex)
+                        + otherData.getLong(otherDurationColumnIndex));
             }
         }
 
@@ -647,7 +768,8 @@
             for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                     componentId++) {
                 totalPowerMah += mData.getDouble(
-                        mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY).mPowerColumnIndex);
+                        mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_ANY, SCREEN_STATE_ANY,
+                                POWER_STATE_ANY).mPowerColumnIndex);
             }
             for (int i = 0; i < mData.layout.customPowerComponentCount; i++) {
                 totalPowerMah += mData.getDouble(
@@ -661,19 +783,17 @@
          */
         @NonNull
         public PowerComponents build() {
-            for (BatteryConsumer.Key[] keys : mData.layout.keys) {
-                for (BatteryConsumer.Key key : keys) {
-                    if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) {
-                        if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) {
-                            mData.putInt(key.mPowerModelColumnIndex,
-                                    BatteryConsumer.POWER_MODEL_UNDEFINED);
-                        }
+            for (BatteryConsumer.Key key: mData.layout.keys) {
+                if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) {
+                    if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) {
+                        mData.putInt(key.mPowerModelColumnIndex,
+                                BatteryConsumer.POWER_MODEL_UNDEFINED);
                     }
+                }
 
-                    if (mMinConsumedPowerThreshold != 0) {
-                        if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) {
-                            mData.putDouble(key.mPowerColumnIndex, 0);
-                        }
+                if (mMinConsumedPowerThreshold != 0) {
+                    if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) {
+                        mData.putDouble(key.mPowerColumnIndex, 0);
                     }
                 }
             }
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index b5029a6..2fde5e7 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -119,7 +119,7 @@
         "PowerComponents\\.java",
         "[^/]*BatteryConsumer[^/]*\\.java"
       ],
-      "name": "BatteryUsageStatsProtoTests"
+      "name": "PowerStatsTests"
     },
     {
       "file_patterns": ["SharedMemory[^/]*\\.java"],
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 53af838..9b5a378 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -140,12 +140,50 @@
                     skipEmptyComponents);
             appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_CACHED,
                     skipEmptyComponents);
-            pw.print(sb);
+            pw.println(sb);
+        } else {
+            pw.println();
         }
 
-        pw.print(" ( ");
-        mPowerComponents.dump(pw, skipEmptyComponents  /* skipTotalPowerComponent */);
-        pw.print(" ) ");
+        pw.print("      ");
+        mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents);
+
+        if (mData.layout.powerStateDataIncluded || mData.layout.screenStateDataIncluded) {
+            for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) {
+                if (mData.layout.powerStateDataIncluded && powerState == POWER_STATE_UNSPECIFIED) {
+                    continue;
+                }
+
+                for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) {
+                    if (mData.layout.screenStateDataIncluded
+                            && screenState == POWER_STATE_UNSPECIFIED) {
+                        continue;
+                    }
+
+                    final double consumedPower = mPowerComponents.getConsumedPower(
+                            POWER_COMPONENT_ANY,
+                            PROCESS_STATE_ANY, screenState, powerState);
+                    if (consumedPower == 0) {
+                        continue;
+                    }
+
+                    pw.print("      (");
+                    if (powerState != POWER_STATE_UNSPECIFIED) {
+                        pw.print(BatteryConsumer.powerStateToString(powerState));
+                    }
+                    if (screenState != SCREEN_STATE_UNSPECIFIED) {
+                        if (powerState != POWER_STATE_UNSPECIFIED) {
+                            pw.print(", ");
+                        }
+                        pw.print("screen ");
+                        pw.print(BatteryConsumer.screenStateToString(screenState));
+                    }
+                    pw.print(") ");
+                    mPowerComponents.dump(pw, screenState, powerState,
+                            skipEmptyComponents  /* skipTotalPowerComponent */);
+                }
+            }
+        }
     }
 
     private void appendProcessStateData(StringBuilder sb, @ProcessState int processState,
@@ -160,10 +198,6 @@
                 .append(BatteryStats.formatCharge(power));
     }
 
-    static UidBatteryConsumer create(BatteryConsumerData data) {
-        return new UidBatteryConsumer(data);
-    }
-
     /** Serializes this object to XML */
     void writeToXml(TypedXmlSerializer serializer) throws IOException {
         if (getConsumedPower() == 0) {
diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java
index 23ba0c6..ea2be7b 100644
--- a/core/java/android/os/UserBatteryConsumer.java
+++ b/core/java/android/os/UserBatteryConsumer.java
@@ -60,10 +60,10 @@
         pw.print("User ");
         pw.print(getUserId());
         pw.print(": ");
-        pw.print(BatteryStats.formatCharge(consumedPower));
-        pw.print(" ( ");
-        mPowerComponents.dump(pw, skipEmptyComponents  /* skipTotalPowerComponent */);
-        pw.print(" ) ");
+        pw.println(BatteryStats.formatCharge(consumedPower));
+        pw.print("      ");
+        mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY,
+                skipEmptyComponents  /* skipTotalPowerComponent */);
     }
 
     /** Serializes this object to XML */
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
index 5d84d17..b07534f 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/text/ClientFlags.java
@@ -68,4 +68,11 @@
     public static boolean fixMisalignedContextMenu() {
         return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU);
     }
+
+    /**
+     * @see Flags#clearFontVariationSettings()
+     */
+    public static boolean clearFontVariationSettings() {
+        return TextFlags.isFeatureEnabled(Flags.FLAG_CLEAR_FONT_VARIATION_SETTINGS);
+    }
 }
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index 9e02460..4dca284 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -61,6 +61,7 @@
             Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE,
             Flags.FLAG_ICU_BIDI_MIGRATION,
             Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU,
+            Flags.FLAG_CLEAR_FONT_VARIATION_SETTINGS,
     };
 
     /**
@@ -75,6 +76,7 @@
             Flags.fixLineHeightForLocale(),
             Flags.icuBidiMigration(),
             Flags.fixMisalignedContextMenu(),
+            Flags.clearFontVariationSettings(),
     };
 
     /**
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 8836c8a..6b1ea26 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -220,3 +220,14 @@
   is_fixed_read_only: true
   bug: "346915432"
 }
+
+flag {
+  name: "clear_font_variation_settings"
+  namespace: "text"
+  description: "The font variation settings must be cleared when the new Typeface is set"
+  bug: "353609778"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
diff --git a/core/java/android/text/format/DateIntervalFormat.java b/core/java/android/text/format/DateIntervalFormat.java
index e8236fd..8dea322 100644
--- a/core/java/android/text/format/DateIntervalFormat.java
+++ b/core/java/android/text/format/DateIntervalFormat.java
@@ -26,6 +26,7 @@
 import android.util.LruCache;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.libcore.Flags;
 
 import java.text.FieldPosition;
 import java.util.TimeZone;
@@ -123,4 +124,14 @@
                 && c.get(Calendar.SECOND) == 0
                 && c.get(Calendar.MILLISECOND) == 0;
     }
+
+
+    @VisibleForTesting(visibility = PACKAGE)
+    public static boolean isLibcoreVFlagEnabled() {
+        // Note that the Flags class is expected to be jarjar-ed in the build-time.
+        // See go/repackage_flags
+        // The full-qualified name should be like
+        // com.android.internal.hidden_from_bootclasspath.com.android.libcore.Flags
+        return Flags.vApis();
+    }
 }
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index f0e673b..7e24749 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -41,6 +41,7 @@
 import android.view.animation.AnimationUtils;
 
 import java.io.PrintWriter;
+import java.util.Locale;
 
 /**
  * Coordinates the timing of animations, input and drawing.
@@ -200,6 +201,7 @@
     private final DisplayEventReceiver.VsyncEventData mLastVsyncEventData =
             new DisplayEventReceiver.VsyncEventData();
     private final FrameData mFrameData = new FrameData();
+    private volatile boolean mInDoFrameCallback = false;
 
     /**
      * Contains information about the current frame for jank-tracking,
@@ -818,6 +820,11 @@
      * @hide
      */
     public long getVsyncId() {
+        if (!mInDoFrameCallback && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+            String message = String.format(Locale.getDefault(), "unsync-vsync-id=%d isSfChoreo=%s",
+                    mLastVsyncEventData.preferredFrameTimeline().vsyncId, this == getSfInstance());
+            Trace.instant(Trace.TRACE_TAG_VIEW, message);
+        }
         return mLastVsyncEventData.preferredFrameTimeline().vsyncId;
     }
 
@@ -853,6 +860,7 @@
             if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                 Trace.traceBegin(
                         Trace.TRACE_TAG_VIEW, "Choreographer#doFrame " + timeline.mVsyncId);
+                mInDoFrameCallback = true;
             }
             synchronized (mLock) {
                 if (!mFrameScheduled) {
@@ -947,6 +955,7 @@
             doCallbacks(Choreographer.CALLBACK_COMMIT, frameIntervalNanos);
         } finally {
             AnimationUtils.unlockAnimationClock();
+            mInDoFrameCallback = false;
             if (resynced) {
                 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
             }
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 1d950dc..6343313 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -119,9 +119,11 @@
 
     @Override
     public boolean applyLocalVisibilityOverride() {
-        ImeTracing.getInstance().triggerClientDump(
-                "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
-                mController.getHost().getInputMethodManager(), null /* icProto */);
+        if (!Flags.refactorInsetsController()) {
+            ImeTracing.getInstance().triggerClientDump(
+                    "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                    mController.getHost().getInputMethodManager(), null /* icProto */);
+        }
         return super.applyLocalVisibilityOverride();
     }
 
@@ -205,9 +207,13 @@
 
     @Override
     public void removeSurface() {
-        final IBinder window = mController.getHost().getWindowToken();
-        if (window != null) {
-            getImm().removeImeSurface(window);
+        if (Flags.refactorInsetsController()) {
+            super.removeSurface();
+        } else {
+            final IBinder window = mController.getHost().getWindowToken();
+            if (window != null) {
+                getImm().removeImeSurface(window);
+            }
         }
     }
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index df2af73..f166b89 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -765,7 +765,7 @@
 
     public InsetsController(Host host) {
         this(host, (controller, id, type) -> {
-            if (type == ime()) {
+            if (!Flags.refactorInsetsController() &&  type == ime()) {
                 return new ImeInsetsSourceConsumer(id, controller.mState,
                         Transaction::new, controller);
             } else {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index c73cbc6..477e35b 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -43,6 +43,7 @@
 import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.ImeTracing;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -296,6 +297,13 @@
 
     @VisibleForTesting(visibility = PACKAGE)
     public boolean applyLocalVisibilityOverride() {
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                ImeTracing.getInstance().triggerClientDump(
+                        "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                        mController.getHost().getInputMethodManager(), null /* icProto */);
+            }
+        }
         final InsetsSource source = mState.peekSource(mId);
         if (source == null) {
             return false;
@@ -396,6 +404,14 @@
      */
     public void removeSurface() {
         // no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                final IBinder window = mController.getHost().getWindowToken();
+                if (window != null) {
+                    mController.getHost().getInputMethodManager().removeImeSurface(window);
+                }
+            }
+        }
     }
 
     @VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 82a7e16..a23e383 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
 import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+import static android.view.accessibility.Flags.removeChildHoverCheckForTouchExploration;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW;
 import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN;
@@ -17486,9 +17487,9 @@
      * Dispatching hover events to {@link TouchDelegate} to improve accessibility.
      * <p>
      * This method is dispatching hover events to the delegate target to support explore by touch.
-     * Similar to {@link ViewGroup#dispatchTouchEvent}, this method send proper hover events to
+     * Similar to {@link ViewGroup#dispatchTouchEvent}, this method sends proper hover events to
      * the delegate target according to the pointer and the touch area of the delegate while touch
-     * exploration enabled.
+     * exploration is enabled.
      * </p>
      *
      * @param event The motion event dispatch to the delegate target.
@@ -17520,17 +17521,33 @@
         // hover events but receive accessibility focus, it should also not delegate to these
         // views when hovered.
         if (!oldHoveringTouchDelegate) {
-            if ((action == MotionEvent.ACTION_HOVER_ENTER
-                    || action == MotionEvent.ACTION_HOVER_MOVE)
-                    && !pointInHoveredChild(event)
-                    && pointInDelegateRegion) {
-                mHoveringTouchDelegate = true;
+            if (removeChildHoverCheckForTouchExploration()) {
+                if ((action == MotionEvent.ACTION_HOVER_ENTER
+                        || action == MotionEvent.ACTION_HOVER_MOVE) && pointInDelegateRegion) {
+                    mHoveringTouchDelegate = true;
+                }
+            } else {
+                if ((action == MotionEvent.ACTION_HOVER_ENTER
+                        || action == MotionEvent.ACTION_HOVER_MOVE)
+                        && !pointInHoveredChild(event)
+                        && pointInDelegateRegion) {
+                    mHoveringTouchDelegate = true;
+                }
             }
         } else {
-            if (action == MotionEvent.ACTION_HOVER_EXIT
-                    || (action == MotionEvent.ACTION_HOVER_MOVE
+            if (removeChildHoverCheckForTouchExploration()) {
+                if (action == MotionEvent.ACTION_HOVER_EXIT
+                        || (action == MotionEvent.ACTION_HOVER_MOVE)) {
+                    if (!pointInDelegateRegion) {
+                        mHoveringTouchDelegate = false;
+                    }
+                }
+            } else {
+                if (action == MotionEvent.ACTION_HOVER_EXIT
+                        || (action == MotionEvent.ACTION_HOVER_MOVE
                         && (pointInHoveredChild(event) || !pointInDelegateRegion))) {
-                mHoveringTouchDelegate = false;
+                    mHoveringTouchDelegate = false;
+                }
             }
         }
         switch (action) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 596726f..3a1d833 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1135,6 +1135,8 @@
     // Take 24 and 30 as an example, 24 is not a divisor of 30.
     // We consider there is a conflict.
     private boolean mIsFrameRateConflicted = false;
+    // Used to check whether SurfaceControl has been replaced.
+    private boolean mSurfaceReplaced = false;
     // Used to set frame rate compatibility.
     @Surface.FrameRateCompatibility int mFrameRateCompatibility =
             FRAME_RATE_COMPATIBILITY_FIXED_SOURCE;
@@ -3831,6 +3833,7 @@
                 surfaceReplaced = (surfaceGenerationId != mSurface.getGenerationId()
                         || surfaceControlChanged) && mSurface.isValid();
                 if (surfaceReplaced) {
+                    mSurfaceReplaced = true;
                     mSurfaceSequenceId++;
                 }
                 if (alwaysConsumeSystemBarsChanged) {
@@ -4443,6 +4446,7 @@
             mPreferredFrameRate = -1;
             mIsFrameRateConflicted = false;
             mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN;
+            mSurfaceReplaced = false;
         } else if (mPreferredFrameRate == 0) {
             // From MSG_FRAME_RATE_SETTING, where mPreferredFrameRate is set to 0
             setPreferredFrameRate(0);
@@ -7543,7 +7547,6 @@
                             animationCallback.onBackCancelled();
                         } else {
                             topCallback.onBackInvoked();
-                            return FINISH_HANDLED;
                         }
                         break;
                 }
@@ -7551,14 +7554,16 @@
                 if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
                     if (!keyEvent.isCanceled()) {
                         topCallback.onBackInvoked();
-                        return FINISH_HANDLED;
                     } else {
                         Log.d(mTag, "Skip onBackInvoked(), reason: keyEvent.isCanceled=true");
                     }
                 }
             }
-
-            return FINISH_NOT_HANDLED;
+            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                // forward a cancelled event so that following stages cancel their back logic
+                keyEvent.cancel();
+            }
+            return FORWARD;
         }
 
         @Override
@@ -12933,8 +12938,9 @@
 
         boolean traceFrameRateCategory = false;
         try {
-            if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT
-                    && mLastPreferredFrameRateCategory != frameRateCategory) {
+            if ((frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT
+                    && mLastPreferredFrameRateCategory != frameRateCategory)
+                    || mSurfaceReplaced) {
                 traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
                 if (traceFrameRateCategory) {
                     String reason = reasonToString(frameRateReason);
@@ -12998,7 +13004,7 @@
 
         boolean traceFrameRate = false;
         try {
-            if (mLastPreferredFrameRate != preferredFrameRate) {
+            if (mLastPreferredFrameRate != preferredFrameRate || mSurfaceReplaced) {
                 traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
                 if (traceFrameRate) {
                     Trace.traceBegin(
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 561d979..987c8c8 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -1708,6 +1708,7 @@
                 }
                 mTypeBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
             }
+            mSystemInsetsConsumed = false;
             return this;
         }
 
@@ -1736,6 +1737,7 @@
                 }
                 mTypeMaxBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
             }
+            mStableInsetsConsumed = false;
             return this;
         }
 
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index a5ba294..90cfcb1 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -982,6 +982,7 @@
     private long mParentNodeId = UNDEFINED_NODE_ID;
     private long mLabelForId = UNDEFINED_NODE_ID;
     private long mLabeledById = UNDEFINED_NODE_ID;
+    private LongArray mLabeledByIds;
     private long mTraversalBefore = UNDEFINED_NODE_ID;
     private long mTraversalAfter = UNDEFINED_NODE_ID;
 
@@ -3599,6 +3600,131 @@
     }
 
     /**
+     * Adds the view which serves as the label of the view represented by
+     * this info for accessibility purposes. When more than one labels are
+     * added, the content from each label is combined in the order that
+     * they are added.
+     * <p>
+     * If visible text can be used to describe or give meaning to this UI,
+     * this method is preferred. For example, a TextView before an EditText
+     * in the UI usually specifies what information is contained in the
+     * EditText. Hence, the EditText is labelled by the TextView.
+     * </p>
+     *
+     * @param label A view that labels this node's source.
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
+    public void addLabeledBy(@NonNull View label) {
+        addLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID);
+    }
+
+    /**
+     * Adds the view which serves as the label of the view represented by
+     * this info for accessibility purposes. If <code>virtualDescendantId</code>
+     * is {@link View#NO_ID} the root is set as the label. When more than one
+     * labels are added, the content from each label is combined in the order
+     * that they are added.
+     * <p>
+     * A virtual descendant is an imaginary View that is reported as a part of the view
+     * hierarchy for accessibility purposes. This enables custom views that draw complex
+     * content to report themselves as a tree of virtual views, thus conveying their
+     * logical structure.
+     * </p>
+     * <p>
+     * If visible text can be used to describe or give meaning to this UI,
+     * this method is preferred. For example, a TextView before an EditText
+     * in the UI usually specifies what information is contained in the
+     * EditText. Hence, the EditText is labelled by the TextView.
+     * </p>
+     * <p>
+     *   <strong>Note:</strong> Cannot be called from an
+     *   {@link android.accessibilityservice.AccessibilityService}.
+     *   This class is made immutable before being delivered to an AccessibilityService.
+     * </p>
+     *
+     * @param root A root whose virtual descendant labels this node's source.
+     * @param virtualDescendantId The id of the virtual descendant.
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
+    public void addLabeledBy(@NonNull View root, int virtualDescendantId) {
+        enforceNotSealed();
+        Preconditions.checkNotNull(root, "%s must not be null", root);
+        if (mLabeledByIds == null) {
+            mLabeledByIds = new LongArray();
+        }
+        mLabeledById = makeNodeId(root.getAccessibilityViewId(), virtualDescendantId);
+        mLabeledByIds.add(mLabeledById);
+    }
+
+    /**
+     * Gets the list of node infos which serve as the labels of the view represented by
+     * this info for accessibility purposes.
+     *
+     * @return The list of labels in the order that they were added.
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
+    public @NonNull List<AccessibilityNodeInfo> getLabeledByList() {
+        enforceSealed();
+        List<AccessibilityNodeInfo> labels = new ArrayList<>();
+        if (mLabeledByIds == null) {
+            return labels;
+        }
+        for (int i = 0; i < mLabeledByIds.size(); i++) {
+            labels.add(getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledByIds.get(i)));
+        }
+        return labels;
+    }
+
+    /**
+     * Removes a label. If the label was not previously added to the node,
+     * calling this method has no effect.
+     * <p>
+     * <strong>Note:</strong> Cannot be called from an
+     * {@link android.accessibilityservice.AccessibilityService}.
+     * This class is made immutable before being delivered to an AccessibilityService.
+     * </p>
+     *
+     * @param label The node which serves as this node's label.
+     * @return true if the label was present
+     * @see #addLabeledBy(View)
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
+    public boolean removeLabeledBy(@NonNull View label) {
+        return removeLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID);
+    }
+
+    /**
+     * Removes a virtual label which is a descendant of the given
+     * <code>root</code>. If the label was not previously added to the node,
+     * calling this method has no effect.
+     *
+     * @param root The root of the virtual subtree.
+     * @param virtualDescendantId The id of the virtual node which serves as this node's label.
+     * @return true if the label was present
+     * @see #addLabeledBy(View, int)
+     */
+    @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY)
+    public boolean removeLabeledBy(@NonNull View root, int virtualDescendantId) {
+        enforceNotSealed();
+        final LongArray labeledByIds = mLabeledByIds;
+        if (labeledByIds == null) {
+            return false;
+        }
+        final int rootAccessibilityViewId =
+                (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+        final long labeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+        if (mLabeledById == labeledById) {
+            mLabeledById = UNDEFINED_NODE_ID;
+        }
+        final int index = labeledByIds.indexOf(labeledById);
+        if (index < 0) {
+            return false;
+        }
+        labeledByIds.remove(index);
+        return true;
+    }
+
+    /**
      * Sets the view which serves as the label of the view represented by
      * this info for accessibility purposes.
      *
@@ -3631,7 +3757,17 @@
         enforceNotSealed();
         final int rootAccessibilityViewId = (root != null)
                 ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID;
+        if (Flags.supportMultipleLabeledby()) {
+            if (mLabeledByIds == null) {
+                mLabeledByIds = new LongArray();
+            } else {
+                mLabeledByIds.clear();
+            }
+        }
         mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
+        if (Flags.supportMultipleLabeledby()) {
+            mLabeledByIds.add(mLabeledById);
+        }
     }
 
     /**
@@ -4242,6 +4378,12 @@
         fieldIndex++;
         if (mLabeledById != DEFAULT.mLabeledById) nonDefaultFields |= bitAt(fieldIndex);
         fieldIndex++;
+        if (Flags.supportMultipleLabeledby()) {
+            if (!LongArray.elementsEqual(mLabeledByIds, DEFAULT.mLabeledByIds)) {
+                nonDefaultFields |= bitAt(fieldIndex);
+            }
+            fieldIndex++;
+        }
         if (mTraversalBefore != DEFAULT.mTraversalBefore) nonDefaultFields |= bitAt(fieldIndex);
         fieldIndex++;
         if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex);
@@ -4383,6 +4525,20 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mParentNodeId);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabelForId);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById);
+        if (Flags.supportMultipleLabeledby()) {
+            if (isBitSet(nonDefaultFields, fieldIndex++)) {
+                final LongArray labeledByIds = mLabeledByIds;
+                if (labeledByIds == null) {
+                    parcel.writeInt(0);
+                } else {
+                    final int labeledByIdsSize = labeledByIds.size();
+                    parcel.writeInt(labeledByIdsSize);
+                    for (int i = 0; i < labeledByIdsSize; i++) {
+                        parcel.writeLong(labeledByIds.get(i));
+                    }
+                }
+            }
+        }
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore);
         if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter);
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
@@ -4550,6 +4706,9 @@
         mParentNodeId = other.mParentNodeId;
         mLabelForId = other.mLabelForId;
         mLabeledById = other.mLabeledById;
+        if (Flags.supportMultipleLabeledby()) {
+            mLabeledByIds = other.mLabeledByIds;
+        }
         mTraversalBefore = other.mTraversalBefore;
         mTraversalAfter = other.mTraversalAfter;
         mMinDurationBetweenContentChanges = other.mMinDurationBetweenContentChanges;
@@ -4656,6 +4815,20 @@
         if (isBitSet(nonDefaultFields, fieldIndex++)) mParentNodeId = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mLabelForId = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong();
+        if (Flags.supportMultipleLabeledby()) {
+            if (isBitSet(nonDefaultFields, fieldIndex++)) {
+                final int labeledByIdsSize = parcel.readInt();
+                if (labeledByIdsSize <= 0) {
+                    mLabeledByIds = null;
+                } else {
+                    mLabeledByIds = new LongArray(labeledByIdsSize);
+                    for (int i = 0; i < labeledByIdsSize; i++) {
+                        final long labeledById = parcel.readLong();
+                        mLabeledByIds.add(labeledById);
+                    }
+                }
+            }
+        }
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong();
         if (isBitSet(nonDefaultFields, fieldIndex++)) {
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index d0bc57b..ed2bf79 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -128,6 +128,13 @@
 }
 
 flag {
+    namespace: "accessibility"
+    name: "remove_child_hover_check_for_touch_exploration"
+    description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants"
+    bug: "304770837"
+}
+
+flag {
     name: "skip_accessibility_warning_dialog_for_trusted_services"
     namespace: "accessibility"
     description: "Skips showing the accessibility warning dialog for trusted services."
@@ -177,6 +184,13 @@
 }
 
 flag {
+    name: "support_multiple_labeledby"
+    namespace: "accessibility"
+    description: "Feature flag for supporting multiple labels in AccessibilityNodeInfo labeledby api"
+    bug: "333780959"
+}
+
+flag {
     name: "support_system_pinch_zoom_opt_out_apis"
     namespace: "accessibility"
     description: "Feature flag for declaring system pinch zoom opt-out apis"
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index 5b1c7d5..0ab51e4 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -563,7 +563,7 @@
         return DeviceConfig.getBoolean(
                 DeviceConfig.NAMESPACE_AUTOFILL,
                 DEVICE_CONFIG_ENABLE_RELAYOUT,
-                true);
+                false);
     }
 
     /** @hide */
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 098f655..0e66f7a 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -891,12 +891,13 @@
     @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API)
     @Nullable
     public Intent createImeLanguageSettingsActivityIntent() {
-        if (TextUtils.isEmpty(mLanguageSettingsActivityName)) {
+        final var activityName = !TextUtils.isEmpty(mLanguageSettingsActivityName)
+                ? mLanguageSettingsActivityName : mSettingsActivityName;
+        if (TextUtils.isEmpty(activityName)) {
             return null;
         }
         return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent(
-                new ComponentName(getServiceInfo().packageName,
-                        mLanguageSettingsActivityName)
+                new ComponentName(getServiceInfo().packageName, activityName)
         );
     }
 
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index c9d2eec..fed8eea 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1352,12 +1352,16 @@
                 case MSG_SET_VISIBILITY:
                     final boolean visible = msg.arg1 != 0;
                     synchronized (mH) {
-                        if (visible) {
-                            showSoftInput(mServedView, /* flags */ 0);
-                        } else {
-                            if (mCurRootView != null
-                                    && mCurRootView.getInsetsController() != null) {
-                                mCurRootView.getInsetsController().hide(WindowInsets.Type.ime());
+                        if (mCurRootView != null) {
+                            final var insetsController = mCurRootView.getInsetsController();
+                            if (insetsController != null) {
+                                if (visible) {
+                                    insetsController.show(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                } else {
+                                    insetsController.hide(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                }
                             }
                         }
                     }
@@ -2334,16 +2338,18 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             if (Flags.refactorInsetsController()) {
+                final var viewRootImpl = view.getViewRootImpl();
                 // In case of a running show IME animation, it should not be requested visible,
                 // otherwise the animation would jump and not be controlled by the user anymore
-                if ((mCurRootView.getInsetsController().computeUserAnimatingTypes()
-                        & WindowInsets.Type.ime()) == 0) {
+                if (viewRootImpl != null
+                        && (viewRootImpl.getInsetsController().computeUserAnimatingTypes()
+                                & WindowInsets.Type.ime()) == 0) {
                     // TODO(b/322992891) handle case of SHOW_IMPLICIT
-                    view.getWindowInsetsController().show(WindowInsets.Type.ime());
+                    viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(),
+                            false /* fromIme */, statsToken);
                     return true;
-                } else {
-                    return false;
                 }
+                return false;
             } else {
                 // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
                 // TODO(b/229426865): call WindowInsetsController#show instead.
@@ -2497,7 +2503,10 @@
 
             if (Flags.refactorInsetsController()) {
                 // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY
-                servedView.getWindowInsetsController().hide(WindowInsets.Type.ime());
+                final var viewRootImpl = servedView.getViewRootImpl();
+                if (viewRootImpl != null) {
+                    viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime());
+                }
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 6629fdc4..16727c3 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -23,6 +23,9 @@
 import android.os.Parcelable;
 import android.util.Base64;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * @hide
  */
@@ -80,6 +83,35 @@
         out.writeTypedArray(signatures, 0);
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o instanceof WebViewProviderInfo that) {
+            return this.packageName.equals(that.packageName)
+                    && this.description.equals(that.description)
+                    && this.availableByDefault == that.availableByDefault
+                    && this.isFallback == that.isFallback
+                    && Arrays.equals(this.signatures, that.signatures);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageName, description, availableByDefault,
+                isFallback, Arrays.hashCode(signatures));
+    }
+
+    @Override
+    public String toString() {
+        return "WebViewProviderInfo; packageName=" + packageName
+                + " description=\"" + description
+                + "\" availableByDefault=" + availableByDefault
+                + " isFallback=" + isFallback
+                + " signatures=" + Arrays.toString(signatures);
+    }
+
     // fields read from framework resource
     public final String packageName;
     public final String description;
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 4c8bad6..205f1de 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -412,8 +412,7 @@
         final WindowManager.LayoutParams attrs = Flags.drawSnapshotAspectRatioMatch()
                 ? info.mainWindowLayoutParams : info.topOpaqueWindowLayoutParams;
         final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams;
-        final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState;
-        if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) {
+        if (attrs == null || mainWindowParams == null) {
             Log.w(TAG, "unable to create taskSnapshot surface ");
             return null;
         }
@@ -456,7 +455,10 @@
         return layoutParams;
     }
 
-    static Rect getSystemBarInsets(Rect frame, InsetsState state) {
+    static Rect getSystemBarInsets(Rect frame, @Nullable InsetsState state) {
+        if (state == null) {
+            return new Rect();
+        }
         return state.calculateInsets(frame, WindowInsets.Type.systemBars(),
                 false /* ignoreVisibility */).toRect();
     }
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 3f1c06a..91ac4ff 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -101,6 +101,13 @@
 }
 
 flag {
+    name: "enable_cascading_windows"
+    namespace: "lse_desktop_experience"
+    description: "Whether to apply cascading effect for placing multiple windows when first launched"
+    bug: "325240051"
+}
+
+flag {
     name: "enable_camera_compat_for_desktop_windowing"
     namespace: "lse_desktop_experience"
     description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode"
@@ -129,6 +136,13 @@
 }
 
 flag {
+    name: "enable_caption_compat_inset_force_consumption_always"
+    namespace: "lse_desktop_experience"
+    description: "Enables force-consumption of caption bar insets for all apps in freeform"
+    bug: "352563889"
+}
+
+flag {
     name: "show_desktop_windowing_dev_option"
     namespace: "lse_desktop_experience"
     description: "Whether to show developer option for enabling desktop windowing mode"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 5397e91..c451cc8 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -19,6 +19,16 @@
 }
 
 flag {
+    name: "do_not_skip_ime_by_target_visibility"
+    namespace: "windowing_frontend"
+    description: "Avoid window traversal missing IME"
+    bug: "339375944"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "apply_lifecycle_on_pip_change"
     namespace: "windowing_frontend"
     description: "Make pip activity lifecyle change with windowing mode"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index ae9d757..13d465f 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -3,16 +3,6 @@
 
 # Project link: https://gantry.corp.google.com/projects/android_platform_windowing_sdk/changes
 
-# Using a fixed read only flag because there are ClientTransaction scheduling before
-# WindowManagerService creation.
-flag {
-    namespace: "windowing_sdk"
-    name: "bundle_client_transaction_flag"
-    description: "To bundle multiple ClientTransactionItems into one ClientTransaction"
-    bug: "260873529"
-    is_fixed_read_only: true
-}
-
 flag {
     namespace: "windowing_sdk"
     name: "activity_embedding_overlay_presentation_flag"
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 75ddb58..f9c2947 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -48,6 +48,7 @@
 import android.os.UserHandle;
 import android.os.Vibrator;
 import android.provider.Settings;
+import android.provider.SettingsStringUtil;
 import android.speech.tts.TextToSpeech;
 import android.speech.tts.Voice;
 import android.text.TextUtils;
@@ -151,7 +152,8 @@
      *         info for toggling a framework feature
      */
     public static Map<ComponentName, FrameworkFeatureInfo>
-        getFrameworkShortcutFeaturesMap() {
+            getFrameworkShortcutFeaturesMap() {
+
         if (sFrameworkShortcutFeaturesMap == null) {
             Map<ComponentName, FrameworkFeatureInfo> featuresMap = new ArrayMap<>(4);
             featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
@@ -172,7 +174,7 @@
                                 R.string.one_handed_mode_feature_name));
             }
             featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME,
-                    new ToggleableFrameworkFeatureInfo(
+                    new ExtraDimFrameworkFeatureInfo(
                             Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                             "1" /* Value to enable */, "0" /* Value to disable */,
                             R.string.reduce_bright_colors_feature_name));
@@ -828,6 +830,44 @@
         }
     }
 
+
+    public static class ExtraDimFrameworkFeatureInfo extends FrameworkFeatureInfo {
+        ExtraDimFrameworkFeatureInfo(String settingKey, String settingOnValue,
+                String settingOffValue, int labelStringResourceId) {
+            super(settingKey, settingOnValue, settingOffValue, labelStringResourceId);
+        }
+
+        /**
+         * Perform shortcut action.
+         *
+         * @return True if the accessibility service is enabled, false otherwise.
+         */
+        public boolean activateShortcut(Context context, int userId) {
+            if (com.android.server.display.feature.flags.Flags.evenDimmer()
+                    && context.getResources().getBoolean(
+                    com.android.internal.R.bool.config_evenDimmerEnabled)) {
+                launchExtraDimDialog();
+                return true;
+            } else {
+                // Assuming that the default state will be to have the feature off
+                final SettingsStringUtil.SettingStringHelper
+                        setting = new SettingsStringUtil.SettingStringHelper(
+                        context.getContentResolver(), getSettingKey(), userId);
+                if (!TextUtils.equals(getSettingOnValue(), setting.read())) {
+                    setting.write(getSettingOnValue());
+                    return true;
+                } else {
+                    setting.write(getSettingOffValue());
+                    return false;
+                }
+            }
+        }
+
+        private void launchExtraDimDialog() {
+            // TODO: launch Extra dim dialog for feature migration
+        }
+    }
+
     // Class to allow mocking of static framework calls
     public static class FrameworkObjectProvider {
         public AccessibilityManager getAccessibilityManagerInstance(Context context) {
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 3e6f18e..69d1cb3 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -161,12 +161,12 @@
     public static final int CUJ_DESKTOP_MODE_RESIZE_WINDOW = 106;
 
     /**
-     * Track entering desktop mode interaction via app handle drag.
+     * Track app handle drag and hold interaction.
      *
      * <p>Tracking starts when the app handle is dragged and
-     * finishes when the window animation to desktop ends after app handle release.
+     * finishes immediately after app handle release, before starting a new transition.
      */
-    public static final int CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG = 107;
+    public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD = 107;
 
     /** Track exiting desktop mode interaction. */
     public static final int CUJ_DESKTOP_MODE_EXIT_MODE = 108;
@@ -197,8 +197,21 @@
     /** Track launching an app through the Launcher Keyboard Quick Switch View */
     public static final int CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH = 115;
 
+    /**
+     * Track entering desktop mode interaction via app handle drag release.
+     *
+     * <p>Tracking starts when the app handle is released and
+     * finishes when one of the three possible animations end:
+     * <ul>
+     *     <li>release to desktop</li>
+     *     <li>release to split-screen</li>
+     *     <li>release to back to full-screen</li>
+     * </ul>.
+     */
+    public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE = 116;
+
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
-    @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
+    @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE;
 
     /** @hide */
     @IntDef({
@@ -297,7 +310,7 @@
             CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW,
             CUJ_FOLD_ANIM,
             CUJ_DESKTOP_MODE_RESIZE_WINDOW,
-            CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG,
+            CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD,
             CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU,
             CUJ_DESKTOP_MODE_EXIT_MODE,
             CUJ_DESKTOP_MODE_MINIMIZE_WINDOW,
@@ -305,7 +318,8 @@
             CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
             CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN,
             CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE,
-            CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+            CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
+            CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {}
@@ -414,7 +428,7 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MAXIMIZE_WINDOW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_FOLD_ANIM] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__FOLD_ANIM;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_RESIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_RESIZE_WINDOW;
-        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_MINIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_MINIMIZE_WINDOW;
@@ -423,6 +437,7 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE;
     }
 
     private Cuj() {
@@ -631,8 +646,8 @@
                 return "FOLD_ANIM";
             case CUJ_DESKTOP_MODE_RESIZE_WINDOW:
                 return "DESKTOP_MODE_RESIZE_WINDOW";
-            case CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG:
-                return "DESKTOP_MODE_ENTER_MODE_APP_HANDLE_DRAG";
+            case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD:
+                return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD";
             case CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU:
                 return "DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU";
             case CUJ_DESKTOP_MODE_EXIT_MODE:
@@ -649,6 +664,8 @@
                 return "LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE";
             case CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH:
                 return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH";
+            case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE:
+                return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java
index 24971f5..488e06f 100644
--- a/core/java/com/android/internal/os/PowerStats.java
+++ b/core/java/com/android/internal/os/PowerStats.java
@@ -580,10 +580,15 @@
         }
         PowerStatsFormatter uidStatsFormatter = descriptor.getUidStatsFormatter();
         for (int i = 0; i < uidStats.size(); i++) {
+            String formattedStats = uidStatsFormatter.format(uidStats.valueAt(i));
+            if (formattedStats.isBlank()) {
+                continue;
+            }
+
             pw.print("UID ");
             pw.print(UserHandle.formatUid(uidStats.keyAt(i)));
             pw.print(": ");
-            pw.print(uidStatsFormatter.format(uidStats.valueAt(i)));
+            pw.print(formattedStats);
             pw.println();
         }
         pw.decreaseIndent();
diff --git a/core/java/com/android/internal/os/TEST_MAPPING b/core/java/com/android/internal/os/TEST_MAPPING
index d552e0b..ae43acf 100644
--- a/core/java/com/android/internal/os/TEST_MAPPING
+++ b/core/java/com/android/internal/os/TEST_MAPPING
@@ -18,7 +18,7 @@
         "Kernel[^/]*\\.java",
         "[^/]*Power[^/]*\\.java"
       ],
-      "name": "BatteryUsageStatsProtoTests"
+      "name": "PowerStatsTests"
     },
     {
       "file_patterns": [
diff --git a/core/java/com/android/internal/policy/FoldLockSettingsObserver.java b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java
new file mode 100644
index 0000000..c6fba8a
--- /dev/null
+++ b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.policy;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Set;
+
+/**
+ * A ContentObserver that listens for changes in the "Continue using apps on fold" setting. This
+ * setting determines a device's behavior when the user folds the device.
+ * @hide
+ *
+ * Keep the setting values in this class in sync with the values in
+ * {@link com.android.server.utils.FoldSettingProvider} and
+ * {@link com.android.settings.display.FoldLockBehaviorSettings}
+ */
+public class FoldLockSettingsObserver extends ContentObserver {
+    /** The setting for "stay awake on fold". */
+    public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key";
+    /** The setting for "swipe up to continue". */
+    public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key";
+    /** The setting for "always sleep on fold". */
+    public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key";
+    public static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+    private static final Set<String> SETTING_VALUES = Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD,
+            SETTING_VALUE_SELECTIVE_STAY_AWAKE, SETTING_VALUE_SLEEP_ON_FOLD);
+
+    private final Context mContext;
+
+    /** The cached value of the setting. */
+    @VisibleForTesting
+    String mFoldLockSetting;
+
+    public FoldLockSettingsObserver(Handler handler, Context context) {
+        super(handler);
+        mContext = context;
+    }
+
+    /** Registers the observer and updates the cache for the first time. */
+    public void register() {
+        final ContentResolver r = mContext.getContentResolver();
+        r.registerContentObserver(
+                Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR),
+                false, this, UserHandle.USER_ALL);
+        requestAndCacheFoldLockSetting();
+    }
+
+    /** Unregisters the observer. */
+    public void unregister() {
+        mContext.getContentResolver().unregisterContentObserver(this);
+    }
+
+    /** Runs when settings changes. */
+    @Override
+    public void onChange(boolean selfChange) {
+        requestAndCacheFoldLockSetting();
+    }
+
+    /**
+     * Requests and caches the current FOLD_LOCK_BEHAVIOR setting, which should be one of three
+     * values: SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+     * SETTING_VALUE_SLEEP_ON_FOLD. If null (not set), returns the system default.
+     */
+    @VisibleForTesting
+    void requestAndCacheFoldLockSetting() {
+        String currentSetting = request();
+
+        if (currentSetting == null || !SETTING_VALUES.contains(currentSetting)) {
+            currentSetting = SETTING_VALUE_DEFAULT;
+        }
+
+        setCurrentFoldSetting(currentSetting);
+    }
+
+    /**
+     * Makes a binder call to request the current FOLD_LOCK_BEHAVIOR setting.
+     */
+    @VisibleForTesting
+    @Nullable
+    String request() {
+        return Settings.System.getStringForUser(mContext.getContentResolver(),
+                Settings.System.FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT);
+    }
+
+    /** Caches the fold-lock behavior received from Settings. */
+    @VisibleForTesting
+    void setCurrentFoldSetting(String newSetting) {
+        mFoldLockSetting = newSetting;
+    }
+
+    /** Used by external requesters: checks if the current setting is "stay awake on fold". */
+    public boolean isStayAwakeOnFold() {
+        return mFoldLockSetting.equals(SETTING_VALUE_STAY_AWAKE_ON_FOLD);
+    }
+
+    /** Used by external requesters: checks if the current setting is "swipe up to continue". */
+    public boolean isSelectiveStayAwake() {
+        return mFoldLockSetting.equals(SETTING_VALUE_SELECTIVE_STAY_AWAKE);
+    }
+
+    /** Used by external requesters: checks if the current setting is "sleep on fold". */
+    public boolean isSleepOnFold() {
+        return mFoldLockSetting.equals(SETTING_VALUE_SLEEP_ON_FOLD);
+    }
+}
diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
new file mode 100644
index 0000000..969ed99
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogClient.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 com.android.internal.protolog;
+
+/**
+ * The ProtoLog client interface.
+ *
+ * These clients will communicate bi-directionally with the ProtoLog service
+ * (@see IProtoLogService.aidl) running in the system process.
+ *
+ * {@hide}
+ */
+interface IProtoLogClient {
+    void toggleLogcat(boolean enabled, in String[] groups);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogService.aidl
new file mode 100644
index 0000000..cc349ea
--- /dev/null
+++ b/core/java/com/android/internal/protolog/IProtoLogService.aidl
@@ -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.internal.protolog;
+
+import com.android.internal.protolog.IProtoLogClient;
+
+/**
+ * The ProtoLog Service interface.
+ *
+ * This service runs in system server.
+ *
+ * All ProtoLog clients (in theory one per process) will register themselves to this service.
+ * This service will then serve as the entry point for any shell command based interactions with
+ * protolog and get/forward any required information/actions from/to all the registered ProtoLog
+ * clients.
+ *
+ * Clients will be responsible for directly sending all their trace data to Perfetto without passing
+ * through this service, except viewer config data, the mappings from messages hashes (generated by
+ * the ProtoLog Tool if and when the tool processed the source code). So, this service is
+ * responsible for dumping the viewer config data from all clients. The reason for this is because
+ * we do this on Perfetto's onFlush callback when a tracing instance is stopped. However, if a
+ * client process is frozen then this will not dump; system server, where this service runs cannot
+ * be frozen. Secondly many processes (i.e. apps) will run the same code with the same viewer config
+ * data, so this service allows us to orchestrate which config gets dumped so we don't duplicate
+ * this information in the trace and waste valuable trace space.
+ *
+ * {@hide}
+ */
+interface IProtoLogService {
+    interface IRegisterClientArgs {
+        String[] getGroups();
+        boolean[] getGroupsDefaultLogcatStatus();
+        String getViewerConfigFile();
+    }
+
+    void registerClient(IProtoLogClient client, IRegisterClientArgs args);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 652cba7..fbec1f1 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -88,7 +88,7 @@
 /**
  * A service for the ProtoLog logging system.
  */
-public class PerfettoProtoLogImpl implements IProtoLog {
+public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog {
     private static final String LOG_TAG = "ProtoLog";
     public static final String NULL_STRING = "null";
     private final AtomicInteger mTracingInstances = new AtomicInteger();
@@ -100,6 +100,7 @@
     );
     @Nullable
     private final ProtoLogViewerConfigReader mViewerConfigReader;
+    @Nullable
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
     private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>();
     private final Runnable mCacheUpdater;
@@ -111,13 +112,12 @@
     private final Lock mBackgroundServiceLock = new ReentrantLock();
     private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor();
 
-    public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) {
+    public PerfettoProtoLogImpl(@NonNull String viewerConfigFilePath, Runnable cacheUpdater) {
         this(() -> {
             try {
                 return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
             } catch (FileNotFoundException e) {
-                Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e);
-                return null;
+                throw new RuntimeException("Failed to load viewer config file " + viewerConfigFilePath, e);
             }
         }, cacheUpdater);
     }
@@ -127,7 +127,7 @@
     }
 
     public PerfettoProtoLogImpl(
-            @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
             Runnable cacheUpdater
     ) {
         this(viewerConfigInputStreamProvider,
@@ -203,13 +203,23 @@
         return mTracingInstances.get() > 0;
     }
 
+    @Override
+    public void toggleLogcat(boolean enabled, String[] groups) {
+        final ILogger logger = (message) -> Log.d(LOG_TAG, message);
+        if (enabled) {
+            startLoggingToLogcat(groups, logger);
+        } else {
+            stopLoggingToLogcat(groups, logger);
+        }
+    }
+
     /**
      * Start text logging
      * @param groups Groups to start text logging for
      * @param logger A logger to write status updates to
      * @return status code
      */
-    public int startLoggingToLogcat(String[] groups, ILogger logger) {
+    public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
         if (mViewerConfigReader != null) {
             mViewerConfigReader.loadViewerConfig(groups, logger);
         }
@@ -222,7 +232,7 @@
      * @param logger A logger to write status updates to
      * @return status code
      */
-    public int stopLoggingToLogcat(String[] groups, ILogger logger) {
+    public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) {
         if (mViewerConfigReader != null) {
             mViewerConfigReader.unloadViewerConfig(groups, logger);
         }
@@ -242,13 +252,29 @@
         for (IProtoLogGroup protoLogGroup : protoLogGroups) {
             mLogGroups.put(protoLogGroup.name(), protoLogGroup);
         }
+
+        final String[] groupsLoggingToLogcat = Arrays.stream(protoLogGroups)
+                .filter(IProtoLogGroup::isLogToLogcat)
+                .map(IProtoLogGroup::name)
+                .toArray(String[]::new);
+
+        if (mViewerConfigReader != null) {
+            mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat);
+        }
     }
 
     /**
      * Responds to a shell command.
      */
+    @Deprecated
     public int onShellCommand(ShellCommand shell) {
         PrintWriter pw = shell.getOutPrintWriter();
+
+        if (android.tracing.Flags.clientSideProtoLogging()) {
+            pw.println("Command deprecated. Please use 'cmd protolog' instead.");
+            return -1;
+        }
+
         String cmd = shell.getNextArg();
         if (cmd == null) {
             return unknownCommand(pw);
@@ -742,7 +768,8 @@
         return -1;
     }
 
-    private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) {
+    private synchronized void onTracingInstanceStart(
+            int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
         final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
         for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
             mDefaultLogLevelCounts[i]++;
@@ -775,7 +802,8 @@
         this.mTracingInstances.incrementAndGet();
     }
 
-    private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) {
+    private synchronized void onTracingInstanceStop(
+            int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
         this.mTracingInstances.decrementAndGet();
 
         final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
index 84f3237..6dc6585 100644
--- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java
+++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java
@@ -49,12 +49,12 @@
         ProtoLogDataSource.TlsState,
         ProtoLogDataSource.IncrementalState> {
 
-    private final Consumer<ProtoLogConfig> mOnStart;
+    private final Instance.TracingInstanceStartCallback mOnStart;
     private final Runnable mOnFlush;
-    private final Consumer<ProtoLogConfig> mOnStop;
+    private final Instance.TracingInstanceStopCallback mOnStop;
 
-    public ProtoLogDataSource(Consumer<ProtoLogConfig> onStart, Runnable onFlush,
-            Consumer<ProtoLogConfig> onStop) {
+    public ProtoLogDataSource(Instance.TracingInstanceStartCallback onStart, Runnable onFlush,
+            Instance.TracingInstanceStopCallback onStop) {
         super("android.protolog");
         this.mOnStart = onStart;
         this.mOnFlush = onFlush;
@@ -267,20 +267,30 @@
 
     public static class Instance extends DataSourceInstance {
 
-        private final Consumer<ProtoLogConfig> mOnStart;
+        public interface TracingInstanceStartCallback {
+            void run(int instanceIdx, ProtoLogConfig config);
+        }
+
+        public interface TracingInstanceStopCallback {
+            void run(int instanceIdx, ProtoLogConfig config);
+        }
+
+        private final TracingInstanceStartCallback mOnStart;
         private final Runnable mOnFlush;
-        private final Consumer<ProtoLogConfig> mOnStop;
+        private final TracingInstanceStopCallback mOnStop;
         private final ProtoLogConfig mConfig;
+        private final int mInstanceIndex;
 
         public Instance(
                 DataSource<Instance, TlsState, IncrementalState> dataSource,
                 int instanceIdx,
                 ProtoLogConfig config,
-                Consumer<ProtoLogConfig> onStart,
+                TracingInstanceStartCallback onStart,
                 Runnable onFlush,
-                Consumer<ProtoLogConfig> onStop
+                TracingInstanceStopCallback onStop
         ) {
             super(dataSource, instanceIdx);
+            this.mInstanceIndex = instanceIdx;
             this.mOnStart = onStart;
             this.mOnFlush = onFlush;
             this.mOnStop = onStop;
@@ -289,7 +299,7 @@
 
         @Override
         public void onStart(StartCallbackArguments args) {
-            this.mOnStart.accept(this.mConfig);
+            this.mOnStart.run(this.mInstanceIndex, this.mConfig);
         }
 
         @Override
@@ -299,7 +309,7 @@
 
         @Override
         public void onStop(StopCallbackArguments args) {
-            this.mOnStop.accept(this.mConfig);
+            this.mOnStop.run(this.mInstanceIndex, this.mConfig);
         }
     }
 }
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 3082295..77ca7ce 100644
--- a/core/java/com/android/internal/protolog/ProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java
@@ -30,6 +30,7 @@
 import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.common.ProtoLogToolInjected;
 
+import java.io.File;
 import java.util.TreeMap;
 
 /**
@@ -105,7 +106,15 @@
     public static synchronized IProtoLog getSingleInstance() {
         if (sServiceInstance == null) {
             if (android.tracing.Flags.perfettoProtologTracing()) {
-                sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater);
+                File f = new File(sViewerConfigPath);
+                if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) {
+                    // TODO(b/353530422): Remove - temporary fix to unblock b/352290057
+                    // In so tests the viewer config file might not exist in which we don't
+                    // want to provide config path to the user
+                    sServiceInstance = new PerfettoProtoLogImpl(null, null, sCacheUpdater);
+                } else {
+                    sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater);
+                }
             } else {
                 sServiceInstance = new LegacyProtoLogImpl(
                         sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater);
diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
index bb6c8b7..38ca0d8 100644
--- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
+++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java
@@ -24,12 +24,13 @@
 import java.util.TreeMap;
 
 public class ProtoLogViewerConfigReader {
+    @NonNull
     private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider;
     private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>();
     private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>();
 
     public ProtoLogViewerConfigReader(
-            ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
+            @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) {
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
     }
 
diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
index 334f548..14bc8e4 100644
--- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
+++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java
@@ -16,11 +16,13 @@
 
 package com.android.internal.protolog;
 
+import android.annotation.NonNull;
 import android.util.proto.ProtoInputStream;
 
 public interface ViewerConfigInputStreamProvider {
     /**
      * @return a ProtoInputStream.
      */
+    @NonNull
     ProtoInputStream getInputStream();
 }
diff --git a/core/java/com/android/internal/widget/MaxHeightFrameLayout.java b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
new file mode 100644
index 0000000..d65dddd
--- /dev/null
+++ b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.internal.R;
+
+/**
+ * This custom subclass of FrameLayout enforces that its calculated height be no larger than the
+ * given maximum height (if any).
+ *
+ * @hide
+ */
+public class MaxHeightFrameLayout extends FrameLayout {
+
+    private int mMaxHeight = Integer.MAX_VALUE;
+
+    public MaxHeightFrameLayout(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public MaxHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.MaxHeightFrameLayout, defStyleAttr, defStyleRes);
+        saveAttributeDataForStyleable(context, R.styleable.MaxHeightFrameLayout,
+                attrs, a, defStyleAttr, defStyleRes);
+
+        setMaxHeight(a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_maxHeight,
+                Integer.MAX_VALUE));
+    }
+
+    /**
+     * Gets the maximum height of this view, in pixels.
+     *
+     * @see #setMaxHeight(int)
+     *
+     * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+     */
+    @Px
+    public int getMaxHeight() {
+        return mMaxHeight;
+    }
+
+    /**
+     * Sets the maximum height this view can have.
+     *
+     * @param maxHeight the maximum height, in pixels
+     *
+     * @see #getMaxHeight()
+     *
+     * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+     */
+    public void setMaxHeight(@Px int maxHeight) {
+        mMaxHeight = maxHeight;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (MeasureSpec.getSize(heightMeasureSpec) > mMaxHeight) {
+            final int mode = MeasureSpec.getMode(heightMeasureSpec);
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, mode);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index c07fd38..7c62615 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -27,6 +27,7 @@
 #include <android_media_audiopolicy.h>
 #include <android_os_Parcel.h>
 #include <audiomanager/AudioManager.h>
+#include <android-base/properties.h>
 #include <binder/IBinder.h>
 #include <jni.h>
 #include <media/AidlConversion.h>
@@ -41,8 +42,10 @@
 #include <system/audio_policy.h>
 #include <utils/Log.h>
 
+#include <thread>
 #include <optional>
 #include <sstream>
+#include <memory>
 #include <vector>
 
 #include "android_media_AudioAttributes.h"
@@ -261,6 +264,13 @@
     jfieldID mMixerBehavior;
 } gAudioMixerAttributesField;
 
+static struct {
+    jclass clazz;
+    jmethodID run;
+} gRunnableClassInfo;
+
+static JavaVM* gVm;
+
 static Mutex gLock;
 
 enum AudioError {
@@ -3362,6 +3372,55 @@
     return enabled;
 }
 
+class JavaSystemPropertyListener {
+  public:
+    JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) :
+            mCallback(env->NewGlobalRef(javaCallback)),
+            mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) {
+        mListenerThread = std::thread([this]() mutable {
+            JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm);
+            while (!mCleanupSignal.load()) {
+                using namespace std::chrono_literals;
+                // 1s timeout so this thread can read the cleanup signal to (slowly) be able to
+                // be destroyed.
+                std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: "";
+                if (newVal != "" && mLastVal != newVal) {
+                    threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run);
+                    mLastVal = std::move(newVal);
+                }
+            }
+            });
+    }
+
+    ~JavaSystemPropertyListener() {
+        mCleanupSignal.store(true);
+        mListenerThread.join();
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm);
+        env->DeleteGlobalRef(mCallback);
+    }
+
+  private:
+    jobject mCallback;
+    android::base::CachedProperty mCachedProperty;
+    std::thread mListenerThread;
+    std::atomic<bool> mCleanupSignal{false};
+    std::string mLastVal = "";
+};
+
+std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners;
+std::mutex gSysPropLock{};
+
+static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env,  jobject thiz,
+        jstring sysProp,
+        jobject javaCallback) {
+    ScopedUtfChars sysPropChars{env, sysProp};
+    auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback,
+            std::string{sysPropChars.c_str()});
+    std::unique_lock _l{gSysPropLock};
+    gSystemPropertyListeners.push_back(std::move(listener));
+}
+
+
 // ----------------------------------------------------------------------------
 
 #define MAKE_AUDIO_SYSTEM_METHOD(x) \
@@ -3534,7 +3593,12 @@
                                 android_media_AudioSystem_clearPreferredMixerAttributes),
          MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency),
          MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled),
-         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)};
+         MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled),
+         MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange",
+                                "(Ljava/lang/String;Ljava/lang/Runnable;)V",
+                                android_media_AudioSystem_listenForSystemPropertyChange),
+
+        };
 
 static const JNINativeMethod gEventHandlerMethods[] =
         {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V",
@@ -3816,6 +3880,12 @@
     gAudioMixerAttributesField.mMixerBehavior =
             GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I");
 
+    jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable");
+    gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz);
+    gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V");
+
+    LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0);
+
     AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback);
 
     RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index d32486c..240be3f 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -415,7 +415,7 @@
         env->DeleteLocalRef(pointerCoordsObj);
     }
 
-    event->addSample(eventTimeNanos, rawPointerCoords.data());
+    event->addSample(eventTimeNanos, rawPointerCoords.data(), event->getId());
     event->setMetaState(event->getMetaState() | metaState);
 }
 
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 30c926c..020b27e 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -17,6 +17,8 @@
 #include <android-base/logging.h>
 #include <android-base/properties.h>
 #include <android/graphics/jni_runtime.h>
+#include <android_runtime/AndroidRuntime.h>
+#include <jni_wrappers.h>
 #include <nativehelper/JNIHelp.h>
 #include <nativehelper/jni_macros.h>
 #include <unicode/putil.h>
@@ -27,9 +29,6 @@
 #include <unordered_map>
 #include <vector>
 
-#include "android_view_InputDevice.h"
-#include "core_jni_helpers.h"
-#include "jni.h"
 #ifdef _WIN32
 #include <windows.h>
 #else
@@ -38,8 +37,6 @@
 #include <sys/stat.h>
 #endif
 
-#include <iostream>
-
 using namespace std;
 
 /*
@@ -49,20 +46,13 @@
  * (see AndroidRuntime.cpp).
  */
 
-static JavaVM* javaVM;
-static jclass bridge;
-static jclass layoutLog;
-static jmethodID getLogId;
-static jmethodID logMethodId;
-
 extern int register_android_os_Binder(JNIEnv* env);
-extern int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env);
+extern int register_libcore_util_NativeAllocationRegistry(JNIEnv* env);
 
 typedef void (*FreeFunction)(void*);
 
-static void NativeAllocationRegistry_Delegate_nativeApplyFreeFunction(JNIEnv*, jclass,
-                                                                      jlong freeFunction,
-                                                                      jlong ptr) {
+static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*, jclass, jlong freeFunction,
+                                                       jlong ptr) {
     void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr));
     FreeFunction nativeFreeFunction =
             reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
@@ -70,11 +60,11 @@
 }
 
 static JNINativeMethod gMethods[] = {
-        NATIVE_METHOD(NativeAllocationRegistry_Delegate, nativeApplyFreeFunction, "(JJ)V"),
+        NATIVE_METHOD(NativeAllocationRegistry, applyFreeFunction, "(JJ)V"),
 };
 
-int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env) {
-    return jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry_Delegate", gMethods,
+int register_libcore_util_NativeAllocationRegistry(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry", gMethods,
                                     NELEM(gMethods));
 }
 
@@ -156,8 +146,8 @@
         {"android.view.VelocityTracker", REG_JNI(register_android_view_VelocityTracker)},
         {"com.android.internal.util.VirtualRefBasePtr",
          REG_JNI(register_com_android_internal_util_VirtualRefBasePtr)},
-        {"libcore.util.NativeAllocationRegistry_Delegate",
-         REG_JNI(register_libcore_util_NativeAllocationRegistry_Delegate)},
+        {"libcore.util.NativeAllocationRegistry",
+         REG_JNI(register_libcore_util_NativeAllocationRegistry)},
 };
 
 static int register_jni_procs(const std::unordered_map<std::string, RegJNIRec>& jniRegMap,
@@ -168,28 +158,9 @@
         }
     }
 
-    if (register_android_graphics_classes(env) < 0) {
-        return -1;
-    }
-
     return 0;
 }
 
-int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className,
-                                          const JNINativeMethod* gMethods, int numMethods) {
-    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
-}
-
-JNIEnv* AndroidRuntime::getJNIEnv() {
-    JNIEnv* env;
-    if (javaVM->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return nullptr;
-    return env;
-}
-
-JavaVM* AndroidRuntime::getJavaVM() {
-    return javaVM;
-}
-
 static vector<string> parseCsv(const string& csvString) {
     vector<string> result;
     istringstream stream(csvString);
@@ -200,29 +171,6 @@
     return result;
 }
 
-void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file,
-                     unsigned int line, const char* message) {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jint logPrio = severity;
-    jstring tagString = env->NewStringUTF(tag);
-    jstring messageString = env->NewStringUTF(message);
-
-    jobject bridgeLog = env->CallStaticObjectMethod(bridge, getLogId);
-
-    env->CallVoidMethod(bridgeLog, logMethodId, logPrio, tagString, messageString);
-
-    env->DeleteLocalRef(tagString);
-    env->DeleteLocalRef(messageString);
-    env->DeleteLocalRef(bridgeLog);
-}
-
-void LayoutlibAborter(const char* abort_message) {
-    // Layoutlib should not call abort() as it would terminate Studio.
-    // Throw an exception back to Java instead.
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jniThrowRuntimeException(env, "The Android framework has encountered a fatal error");
-}
-
 // This method has been copied/adapted from system/core/init/property_service.cpp
 // If the ro.product.cpu.abilist* properties have not been explicitly
 // set, derive them from ro.system.product.cpu.abilist* properties.
@@ -311,62 +259,49 @@
 #endif
 }
 
-static bool init_icu(const char* dataPath) {
-    void* addr = mmapFile(dataPath);
-    UErrorCode err = U_ZERO_ERROR;
-    udata_setCommonData(addr, &err);
-    if (err != U_ZERO_ERROR) {
-        return false;
-    }
-    return true;
-}
-
-// Creates an array of InputDevice from key character map files
-static void init_keyboard(JNIEnv* env, const vector<string>& keyboardPaths) {
-    jclass inputDevice = FindClassOrDie(env, "android/view/InputDevice");
-    jobjectArray inputDevicesArray =
-            env->NewObjectArray(keyboardPaths.size(), inputDevice, nullptr);
-    int keyboardId = 1;
-
-    for (const string& path : keyboardPaths) {
-        base::Result<std::shared_ptr<KeyCharacterMap>> charMap =
-                KeyCharacterMap::load(path, KeyCharacterMap::Format::BASE);
-
-        InputDeviceInfo info = InputDeviceInfo();
-        info.initialize(keyboardId, 0, 0, InputDeviceIdentifier(),
-                        "keyboard " + std::to_string(keyboardId), true, false,
-                        ui::LogicalDisplayId::DEFAULT);
-        info.setKeyboardType(AINPUT_KEYBOARD_TYPE_ALPHABETIC);
-        info.setKeyCharacterMap(*charMap);
-
-        jobject inputDeviceObj = android_view_InputDevice_create(env, info);
-        if (inputDeviceObj) {
-            env->SetObjectArrayElement(inputDevicesArray, keyboardId - 1, inputDeviceObj);
-            env->DeleteLocalRef(inputDeviceObj);
+// Loads the ICU data file from the location specified in the system property ro.icu.data.path
+static void loadIcuData() {
+    string icuPath = base::GetProperty("ro.icu.data.path", "");
+    if (!icuPath.empty()) {
+        // Set the location of ICU data
+        void* addr = mmapFile(icuPath.c_str());
+        UErrorCode err = U_ZERO_ERROR;
+        udata_setCommonData(addr, &err);
+        if (err != U_ZERO_ERROR) {
+            ALOGE("Unable to load ICU data\n");
         }
-        keyboardId++;
     }
-
-    if (bridge == nullptr) {
-        bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
-        bridge = MakeGlobalRefOrDie(env, bridge);
-    }
-    jmethodID setInputManager = GetStaticMethodIDOrDie(env, bridge, "setInputManager",
-                                                       "([Landroid/view/InputDevice;)V");
-    env->CallStaticVoidMethod(bridge, setInputManager, inputDevicesArray);
-    env->DeleteLocalRef(inputDevicesArray);
 }
 
-} // namespace android
+static int register_android_core_classes(JNIEnv* env) {
+    jclass system = FindClassOrDie(env, "java/lang/System");
+    jmethodID getPropertyMethod =
+            GetStaticMethodIDOrDie(env, system, "getProperty",
+                                   "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
 
-using namespace android;
+    // Get the names of classes that need to register their native methods
+    auto nativesClassesJString =
+            (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
+                                                 env->NewStringUTF("core_native_classes"),
+                                                 env->NewStringUTF(""));
+    const char* nativesClassesArray = env->GetStringUTFChars(nativesClassesJString, nullptr);
+    string nativesClassesString(nativesClassesArray);
+    vector<string> classesToRegister = parseCsv(nativesClassesString);
+    env->ReleaseStringUTFChars(nativesClassesJString, nativesClassesArray);
+
+    if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
+        return JNI_ERR;
+    }
+
+    return 0;
+}
 
 // Called right before aborting by LOG_ALWAYS_FATAL. Print the pending exception.
 void abort_handler(const char* abort_message) {
     ALOGE("About to abort the process...");
 
-    JNIEnv* env = NULL;
-    if (javaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    if (env == nullptr) {
         ALOGE("vm->GetEnv() failed");
         return;
     }
@@ -377,107 +312,98 @@
     ALOGE("Aborting because: %s", abort_message);
 }
 
+// ------------------ Host implementation of AndroidRuntime ------------------
+
+/*static*/ JavaVM* AndroidRuntime::mJavaVM;
+
+/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className,
+                                                     const JNINativeMethod* gMethods,
+                                                     int numMethods) {
+    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
+}
+
+/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() {
+    JNIEnv* env;
+    JavaVM* vm = AndroidRuntime::getJavaVM();
+    if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
+        return nullptr;
+    }
+    return env;
+}
+
+/*static*/ JavaVM* AndroidRuntime::getJavaVM() {
+    return mJavaVM;
+}
+
+/*static*/ int AndroidRuntime::startReg(JNIEnv* env) {
+    if (register_android_core_classes(env) < 0) {
+        return JNI_ERR;
+    }
+    if (register_android_graphics_classes(env) < 0) {
+        return JNI_ERR;
+    }
+    return 0;
+}
+
+void AndroidRuntime::onVmCreated(JNIEnv* env) {
+    env->GetJavaVM(&mJavaVM);
+}
+
+void AndroidRuntime::onStarted() {
+    property_initialize_ro_cpu_abilist();
+    loadIcuData();
+
+    // Use English locale for number format to ensure correct parsing of floats when using strtof
+    setlocale(LC_NUMERIC, "en_US.UTF-8");
+}
+
+void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    // Register native functions.
+    if (startReg(env) < 0) {
+        ALOGE("Unable to register all android native methods\n");
+    }
+    onStarted();
+}
+
+AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength)
+      : mExitWithoutCleanup(false), mArgBlockStart(argBlockStart), mArgBlockLength(argBlockLength) {
+    init_android_graphics();
+}
+
+AndroidRuntime::~AndroidRuntime() {}
+
+// Version of AndroidRuntime to run on host
+class HostRuntime : public AndroidRuntime {
+public:
+    HostRuntime() : AndroidRuntime(nullptr, 0) {}
+
+    void onVmCreated(JNIEnv* env) override {
+        AndroidRuntime::onVmCreated(env);
+        // initialize logging, so ANDROD_LOG_TAGS env variable is respected
+        android::base::InitLogging(nullptr, android::base::StderrLogger, abort_handler);
+    }
+
+    void onStarted() override {
+        AndroidRuntime::onStarted();
+    }
+};
+
+} // namespace android
+
+using namespace android;
+
 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
-    javaVM = vm;
     JNIEnv* env = nullptr;
     if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
         return JNI_ERR;
     }
 
-    __android_log_set_aborter(abort_handler);
+    Vector<String8> args;
+    HostRuntime runtime;
 
-    init_android_graphics();
-
-    // Configuration is stored as java System properties.
-    // Get a reference to System.getProperty
-    jclass system = FindClassOrDie(env, "java/lang/System");
-    jmethodID getPropertyMethod =
-            GetStaticMethodIDOrDie(env, system, "getProperty",
-                                   "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
-
-    // Java system properties that contain LayoutLib config. The initial values in the map
-    // are the default values if the property is not specified.
-    std::unordered_map<std::string, std::string> systemProperties =
-            {{"core_native_classes", ""},
-             {"register_properties_during_load", ""},
-             {"icu.data.path", ""},
-             {"use_bridge_for_logging", ""},
-             {"keyboard_paths", ""}};
-
-    for (auto& [name, defaultValue] : systemProperties) {
-        jstring propertyString =
-                (jstring)env->CallStaticObjectMethod(system, getPropertyMethod,
-                                                     env->NewStringUTF(name.c_str()),
-                                                     env->NewStringUTF(defaultValue.c_str()));
-        const char* propertyChars = env->GetStringUTFChars(propertyString, 0);
-        systemProperties[name] = string(propertyChars);
-        env->ReleaseStringUTFChars(propertyString, propertyChars);
-    }
-    // Get the names of classes that need to register their native methods
-    vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]);
-
-    if (systemProperties["register_properties_during_load"] == "true") {
-        // Set the system properties first as they could be used in the static initialization of
-        // other classes
-        if (register_android_os_SystemProperties(env) < 0) {
-            return JNI_ERR;
-        }
-        classesToRegister.erase(find(classesToRegister.begin(), classesToRegister.end(),
-                                     "android.os.SystemProperties"));
-        bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
-        bridge = MakeGlobalRefOrDie(env, bridge);
-        jmethodID setSystemPropertiesMethod =
-                GetStaticMethodIDOrDie(env, bridge, "setSystemProperties", "()V");
-        env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod);
-        property_initialize_ro_cpu_abilist();
-    }
-
-    if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
-        return JNI_ERR;
-    }
-
-    if (!systemProperties["icu.data.path"].empty()) {
-        // Set the location of ICU data
-        bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str());
-        if (!icuInitialized) {
-            return JNI_ERR;
-        }
-    }
-
-    if (systemProperties["use_bridge_for_logging"] == "true") {
-        layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog");
-        layoutLog = MakeGlobalRefOrDie(env, layoutLog);
-        logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework",
-                                       "(ILjava/lang/String;Ljava/lang/String;)V");
-        if (bridge == nullptr) {
-            bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge");
-            bridge = MakeGlobalRefOrDie(env, bridge);
-        }
-        getLogId = GetStaticMethodIDOrDie(env, bridge, "getLog",
-                                          "()Lcom/android/ide/common/rendering/api/ILayoutLog;");
-        android::base::SetLogger(LayoutlibLogger);
-        android::base::SetAborter(LayoutlibAborter);
-    } else {
-        // initialize logging, so ANDROD_LOG_TAGS env variable is respected
-        android::base::InitLogging(nullptr, android::base::StderrLogger);
-    }
-
-    // Use English locale for number format to ensure correct parsing of floats when using strtof
-    setlocale(LC_NUMERIC, "en_US.UTF-8");
-
-    if (!systemProperties["keyboard_paths"].empty()) {
-        vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]);
-        init_keyboard(env, keyboardPaths);
-    } else {
-        fprintf(stderr, "Skip initializing keyboard\n");
-    }
+    runtime.onVmCreated(env);
+    runtime.start("HostRuntime", args, false);
 
     return JNI_VERSION_1_6;
 }
-
-JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) {
-    JNIEnv* env = nullptr;
-    vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
-    env->DeleteGlobalRef(bridge);
-    env->DeleteGlobalRef(layoutLog);
-}
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index 5a4d6db..12804d4 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -147,6 +147,7 @@
         IGNORED_ON_WIRELESS_CHARGER = 27;
         IGNORED_MISSING_PERMISSION = 28;
         CANCELLED_BY_APP_OPS = 29;
+        CANCELLED_BY_FOREGROUND_USER = 30;
         reserved 17; // prev IGNORED_UNKNOWN_VIBRATION
     }
 }
diff --git a/core/res/res/drawable/ic_ime_switcher_new.xml b/core/res/res/drawable/ic_ime_switcher_new.xml
new file mode 100644
index 0000000..04f4a25
--- /dev/null
+++ b/core/res/res/drawable/ic_ime_switcher_new.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"
+        android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/core/res/res/drawable/input_method_switch_button.xml b/core/res/res/drawable/input_method_switch_button.xml
new file mode 100644
index 0000000..396d81e
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_button.xml
@@ -0,0 +1,42 @@
+<?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.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetTop="6dp"
+    android:insetBottom="6dp">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <corners android:radius="28dp"/>
+                <solid android:color="@color/white"/>
+            </shape>
+        </item>
+
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="28dp"/>
+                <solid android:color="@color/transparent"/>
+                <stroke android:color="?attr/materialColorPrimary"
+                    android:width="1dp"/>
+                <padding android:left="16dp"
+                    android:top="8dp"
+                    android:right="16dp"
+                    android:bottom="8dp"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/core/res/res/drawable/input_method_switch_item_background.xml b/core/res/res/drawable/input_method_switch_item_background.xml
new file mode 100644
index 0000000..eb7a246
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_item_background.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/list_highlight_material">
+    <item android:id="@id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="28dp"/>
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+
+    <item>
+        <selector>
+            <item android:state_activated="true">
+                <shape android:shape="rectangle">
+                    <corners android:radius="28dp"/>
+                    <solid android:color="?attr/materialColorSecondaryContainer"/>
+                </shape>
+            </item>
+        </selector>
+    </item>
+</ripple>
diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml
new file mode 100644
index 0000000..5a4d6b1
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_dialog_new.xml
@@ -0,0 +1,70 @@
+<?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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <com.android.internal.widget.MaxHeightFrameLayout
+        android:layout_width="320dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:maxHeight="373dp">
+
+        <com.android.internal.widget.RecyclerView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingVertical="8dp"
+            android:clipToPadding="false"
+            android:layoutManager="com.android.internal.widget.LinearLayoutManager"/>
+
+    </com.android.internal.widget.MaxHeightFrameLayout>
+
+    <LinearLayout
+        style="?android:attr/buttonBarStyle"
+        android:id="@+id/button_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:paddingHorizontal="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="16dp"
+        android:visibility="gone">
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"/>
+
+        <Button
+            style="?attr/buttonBarButtonStyle"
+            android:id="@+id/button1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/input_method_switch_button"
+            android:layout_gravity="end"
+            android:text="@string/input_method_language_settings"
+            android:fontFamily="google-sans-text"
+            android:textAppearance="?attr/textAppearance"
+            android:visibility="gone"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml
new file mode 100644
index 0000000..16a97c4
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_item_new.xml
@@ -0,0 +1,88 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingHorizontal="16dp"
+    android:paddingBottom="8dp">
+
+    <View
+        android:id="@+id/divider"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?attr/materialColorSurfaceVariant"
+        android:layout_marginStart="20dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="24dp"
+        android:layout_marginBottom="12dp"
+        android:visibility="gone"/>
+
+    <TextView
+        android:id="@+id/header_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="8dp"
+        android:ellipsize="end"
+        android:singleLine="true"
+        android:fontFamily="google-sans-text"
+        android:textAppearance="?attr/textAppearance"
+        android:textColor="?attr/materialColorPrimary"
+        android:visibility="gone"/>
+
+    <LinearLayout
+        android:id="@+id/list_item"
+        android:layout_width="match_parent"
+        android:layout_height="72dp"
+        android:background="@drawable/input_method_switch_item_background"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingStart="20dp"
+        android:paddingEnd="24dp">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="start|center_vertical"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="marquee"
+                android:singleLine="true"
+                android:fontFamily="google-sans-text"
+                android:textAppearance="?attr/textAppearanceListItem"/>
+
+        </LinearLayout>
+
+        <ImageView
+            android:id="@+id/image"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:gravity="center_vertical"
+            android:layout_marginStart="12dp"
+            android:src="@drawable/ic_check_24dp"
+            android:tint="?attr/materialColorOnSurface"
+            android:visibility="gone"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 0975eda..7cc9e13 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5243,6 +5243,11 @@
              the VISIBLE or INVISIBLE state when measuring. Defaults to false. -->
         <attr name="measureAllChildren" format="boolean" />
     </declare-styleable>
+    <!-- @hide -->
+    <declare-styleable name="MaxHeightFrameLayout">
+        <!-- An optional argument to supply a maximum height for this view. -->
+        <attr name="maxHeight" format="dimension" />
+    </declare-styleable>
     <declare-styleable name="ExpandableListView">
         <!-- Indicator shown beside the group View. This can be a stateful Drawable. -->
         <attr name="groupIndicator" format="reference" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index dc3d935..236e7c5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7090,4 +7090,7 @@
     <!-- Whether to show GAIA education screen during account login of private space setup.
          OEM/Partner can explicitly opt to disable the screen. -->
     <bool name="config_enableGaiaEducationInPrivateSpace">true</bool>
+
+    <!-- Whether to enable usb state update via udc sysfs. -->
+    <bool name="config_enableUdcSysfsUsbStateUpdate">false</bool>
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 46b15416..ec865f6 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3880,6 +3880,8 @@
 
     <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. -->
     <string name="select_input_method">Choose input method</string>
+    <!-- Button to access the language settings of the current input method. [CHAR LIMIT=50]-->
+    <string name="input_method_language_settings">Language Settings</string>
     <!-- Summary text of a toggle switch to enable/disable use of the IME while a physical
          keyboard is connected -->
     <string name="show_ime">Keep it on screen while physical keyboard is active</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c50b961..09688f2 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -523,6 +523,7 @@
   <java-symbol type="integer" name="config_defaultAnalogClockSecondsHandFps"/>
   <java-symbol type="bool" name="config_notificationCloseButtonSupported"/>
   <java-symbol type="bool" name="config_enableGaiaEducationInPrivateSpace"/>
+  <java-symbol type="bool" name="config_enableUdcSysfsUsbStateUpdate"/>
 
   <java-symbol type="color" name="tab_indicator_text_v4" />
 
@@ -1577,6 +1578,8 @@
   <java-symbol type="layout" name="input_method" />
   <java-symbol type="layout" name="input_method_extract_view" />
   <java-symbol type="layout" name="input_method_switch_item" />
+  <java-symbol type="layout" name="input_method_switch_item_new" />
+  <java-symbol type="layout" name="input_method_switch_dialog_new" />
   <java-symbol type="layout" name="input_method_switch_dialog_title" />
   <java-symbol type="layout" name="js_prompt" />
   <java-symbol type="layout" name="list_content_simple" />
@@ -2552,6 +2555,7 @@
   <java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" />
   <java-symbol type="drawable" name="ic_ime_nav_back" />
   <java-symbol type="drawable" name="ic_ime_switcher" />
+  <java-symbol type="drawable" name="ic_ime_switcher_new" />
   <java-symbol type="id" name="input_method_nav_back" />
   <java-symbol type="id" name="input_method_nav_buttons" />
   <java-symbol type="id" name="input_method_nav_center_group" />
@@ -5400,6 +5404,7 @@
   <java-symbol type="style" name="Theme.DeviceDefault.DialogWhenLarge" />
   <java-symbol type="style" name="Theme.DeviceDefault.DocumentsUI" />
   <java-symbol type="style" name="Theme.DeviceDefault.InputMethod" />
+  <java-symbol type="style" name="Theme.DeviceDefault.InputMethodSwitcherDialog" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.DarkActionBar" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.FixedSize" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.MinWidth" />
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 382ff04..f5c6738 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -6179,4 +6179,10 @@
         <item name="colorListDivider">@color/list_divider_opacity_device_default_light</item>
         <item name="opacityListDivider">@color/list_divider_opacity_device_default_light</item>
     </style>
+
+    <!--  Device default theme for the Input Method Switcher dialog.  -->
+    <style name="Theme.DeviceDefault.InputMethodSwitcherDialog" parent="Theme.DeviceDefault.Dialog.Alert.DayNight">
+        <item name="windowMinWidthMajor">@null</item>
+        <item name="windowMinWidthMinor">@null</item>
+    </style>
 </resources>
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java
index 9d7d71d..616c72e 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java
@@ -20,8 +20,8 @@
 
 import android.graphics.Matrix;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java
index d7b911d..d0a4141 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java
@@ -22,8 +22,8 @@
 import android.graphics.RectF;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.ApiTest;
 
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
index 4839dd2..013117e 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
@@ -45,8 +45,8 @@
 import android.view.MotionEvent;
 import android.view.autofill.AutofillId;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index ce85a76..61bf137 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -28,9 +28,9 @@
 import android.os.Parcel;
 import android.platform.test.flag.junit.SetFlagsRule;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.frameworks.inputmethodcoretests.R;
 
@@ -135,7 +135,7 @@
 
     private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes)
             throws Exception {
-        final Context context = InstrumentationRegistry.getContext();
+        final Context context = InstrumentationRegistry.getInstrumentation().getContext();
         final ServiceInfo serviceInfo = new ServiceInfo();
         serviceInfo.applicationInfo = context.getApplicationInfo();
         serviceInfo.packageName = context.getPackageName();
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java
index d705724..812b3f5 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java
@@ -24,9 +24,9 @@
 import android.content.Context;
 import android.hardware.display.DisplayManager;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java
index e7b1110..73ff304 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java
@@ -26,8 +26,8 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java
index 5095cad..4c76992 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java
@@ -27,8 +27,8 @@
 import android.os.Parcel;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java
index 47a724d..608dd4d 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java
@@ -22,8 +22,8 @@
 import android.graphics.PointF;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.ApiTest;
 
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java
index a94f877..bb6a944b 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java
@@ -24,8 +24,8 @@
 import android.os.CancellationSignalBeamer;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.ApiTest;
 
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java
index b2eb07c..4cbd7ab 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java
@@ -22,8 +22,8 @@
 import android.graphics.RectF;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.ApiTest;
 
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java
index df63a4a..c1e2197 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java
@@ -22,8 +22,8 @@
 import android.graphics.RectF;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.ApiTest;
 
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java
index f264cc6..724d729 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java
@@ -25,8 +25,8 @@
 import android.os.Parcel;
 import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java
index a626294..9eb5dd1 100644
--- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java
+++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java
@@ -22,8 +22,8 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputConnectionWrapper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java
index 32bfdcb..6ac3639 100644
--- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java
+++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java
@@ -20,8 +20,8 @@
 
 import android.view.WindowManager.LayoutParams;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java
index ba63908..7b0a5da 100644
--- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java
+++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java
@@ -18,8 +18,8 @@
 
 import static org.junit.Assert.assertEquals;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
index cb3f99c..33a46d0 100644
--- a/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
+++ b/core/tests/coretests/src/android/animation/AnimatorSetCallsTest.java
@@ -35,6 +35,8 @@
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
 
 @MediumTest
 public class AnimatorSetCallsTest {
@@ -447,6 +449,43 @@
         mActivity.runOnUiThread(() -> {});
     }
 
+    @Test
+    public void startAfterSeek() throws Throwable {
+        ArrayList<Float> values = new ArrayList<>();
+        AtomicReference<CountDownLatch> drawLatch = new AtomicReference<>(new CountDownLatch(1));
+
+        mActivity.runOnUiThread(() -> {
+            mAnimator.setDuration(300);
+            mAnimator.setInterpolator(null);
+            View view = (View) mAnimator.getTarget();
+            view.getViewTreeObserver().addOnDrawListener(() -> {
+                values.add(view.getTranslationX());
+                drawLatch.get().countDown();
+            });
+            mSet1.setCurrentPlayTime(150);
+        });
+
+        assertTrue(drawLatch.get().await(1, TimeUnit.SECONDS));
+        drawLatch.set(new CountDownLatch(1));
+
+        mActivity.runOnUiThread(() -> {
+            assertEquals(1, values.size());
+            assertEquals(50f, values.get(0), 0.01f);
+            mSet1.start();
+        });
+
+        assertTrue(drawLatch.get().await(1, TimeUnit.SECONDS));
+
+        mActivity.runOnUiThread(() -> {
+            assertTrue(values.size() >= 2);
+            float lastValue = values.get(0);
+            for (int i = 1; i < values.size(); i++) {
+                assertTrue(values.get(i) >= lastValue);
+                lastValue = values.get(i);
+            }
+        });
+    }
+
     private void waitForOnUiThread(PollingCheck.PollingCheckCondition condition) {
         final boolean[] value = new boolean[1];
         PollingCheck.waitFor(() -> {
diff --git a/core/tests/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS
new file mode 100644
index 0000000..1eefb3a
--- /dev/null
+++ b/core/tests/coretests/src/android/animation/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/animation/OWNERS
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index e8a0762..294352e 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -90,7 +90,8 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -117,6 +118,9 @@
     // few sequence numbers the framework used to launch the test activity.
     private static final int BASE_SEQ = 10000000;
 
+    @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
     @Rule(order = 0)
     public final ActivityTestRule<TestActivity> mActivityTestRule =
             new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */,
@@ -133,8 +137,6 @@
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
-
         // Keep track of the original controller, so that it can be used to restore in tearDown()
         // when there is override in some test cases.
         mOriginalWindowTokenClientController = WindowTokenClientController.getInstance();
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index c7060ad..72c4639 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -47,10 +47,12 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 /**
  * Tests for subtypes of {@link ClientTransactionItem}.
@@ -63,6 +65,9 @@
 @Presubmit
 public class ClientTransactionItemTest {
 
+    @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
     @Mock
     private ClientTransactionHandler mHandler;
     @Mock
@@ -89,7 +94,6 @@
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
         mGlobalConfig = new Configuration();
         mConfiguration = new Configuration();
         mActivitiesToBeDestroyed = new ArrayMap<>();
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index d2a444f..f9609fc 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -59,7 +59,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.concurrent.RejectedExecutionException;
 import java.util.function.BiConsumer;
@@ -76,6 +77,8 @@
 public class ClientTransactionListenerControllerTest {
 
     @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
     @Mock
@@ -100,7 +103,6 @@
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
         mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
         mHandler = getInstrumentation().getContext().getMainThreadHandler();
         mController = spy(ClientTransactionListenerController
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 32e611c..918235b 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -49,12 +49,12 @@
 
 import com.android.window.flags.Flags;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.List;
 import java.util.function.Supplier;
@@ -82,6 +82,9 @@
     }
 
     @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Rule
     public SetFlagsRule mSetFlagsRule;
 
     @Mock
@@ -93,11 +96,6 @@
         mSetFlagsRule = new SetFlagsRule(flags);
     }
 
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-    }
-
     // 1. Check if two obtained objects from pool are not the same.
     // 2. Check if the state of the object is cleared after recycling.
     // 3. Check if the same object is obtained from pool after recycling.
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index 73b7447..eb69b9c 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -57,11 +57,13 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -83,6 +85,9 @@
 @Presubmit
 public class TransactionExecutorTests {
 
+    @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
     @Mock
     private ClientTransactionHandler mTransactionHandler;
     @Mock
@@ -98,8 +103,6 @@
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
         mClientRecord = new ActivityClientRecord();
         when(mTransactionHandler.getActivityClient(any())).thenReturn(mClientRecord);
 
diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
index 6998c32..f5e9cc6 100644
--- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
+++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
@@ -26,8 +26,8 @@
 import android.content.res.Configuration;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
index 71c068d..9750de3 100644
--- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
+++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
@@ -35,6 +35,7 @@
 import static android.text.format.DateUtils.FORMAT_UTC;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.icu.util.Calendar;
 import android.icu.util.TimeZone;
@@ -683,4 +684,12 @@
         assertEquals("February 27\u2009\u2013\u2009March 1, 2004",
                 fmt.apply(1077840000000L, 1078185600000L));
     }
+
+    @Test
+    public void testIsLibcoreVFlagEnabled() {
+        // This flag has been fully ramped. It should never be false.
+        assertTrue(DateIntervalFormat.isLibcoreVFlagEnabled());
+        // Call a Android V API in libcore.
+        assertEquals("\ud840\udc2b", Character.toString(0x2002B));
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 47b28899..248db65 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -37,8 +37,12 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.view.WindowManager.BadTokenException;
 import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
 import android.widget.TextView;
 
@@ -46,6 +50,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -61,6 +66,9 @@
 @RunWith(AndroidJUnit4.class)
 public class ImeInsetsSourceConsumerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
     InsetsSourceConsumer mImeConsumer;
     @Spy InsetsController mController;
@@ -112,6 +120,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingControl() {
         // Set null control and then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -141,6 +150,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingLeash() {
         // Set null control, then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -185,6 +195,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectSkip() {
         // Expect IME animation will skipped when the IME is visible at first place.
         verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */,
@@ -192,6 +203,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() {
         // Expect IME animation will not skipped if previously no view focused when gained the
         // window focus and requesting the IME visible next time.
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index b68ff78..62291d4 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -237,8 +237,8 @@
             return;
         }
         waitForFrameRateCategoryToSettle();
-        assertEquals(FRAME_RATE_CATEGORY_LOW,
-                        mViewRoot.getLastPreferredFrameRateCategory());
+        assertTrue(mViewRoot.getLastPreferredFrameRateCategory()
+                < FRAME_RATE_CATEGORY_HIGH_HINT);
 
         int width = mMovingView.getWidth();
         int height = mMovingView.getHeight();
diff --git a/core/tests/coretests/src/android/view/WindowInfoTest.java b/core/tests/coretests/src/android/view/WindowInfoTest.java
index d927f06..43e678f 100644
--- a/core/tests/coretests/src/android/view/WindowInfoTest.java
+++ b/core/tests/coretests/src/android/view/WindowInfoTest.java
@@ -34,8 +34,8 @@
 import android.text.TextUtils;
 import android.view.accessibility.AccessibilityNodeInfo;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java
index ab4543c..ba1204b 100644
--- a/core/tests/coretests/src/android/view/WindowInsetsTest.java
+++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.view.WindowInsets.Type.SIZE;
+import static android.view.WindowInsets.Type.captionBar;
 import static android.view.WindowInsets.Type.systemBars;
 
 import static org.junit.Assert.assertEquals;
@@ -26,12 +27,14 @@
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 @Presubmit
@@ -68,4 +71,17 @@
                 true /* compatIgnoreVisibility */, null, null, 0, 0);
         assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets());
     }
+
+    @Test
+    public void testSetBoundingRectsInBuilder_noInsets_preservedInWindowInsets() {
+        final List<Rect> rects = List.of(new Rect(0, 0, 50, 100));
+        final WindowInsets insets =
+                new WindowInsets.Builder()
+                        .setBoundingRects(captionBar(), rects)
+                        .setBoundingRectsIgnoringVisibility(captionBar(), rects)
+                        .build();
+
+        assertEquals(rects, insets.getBoundingRects(captionBar()));
+        assertEquals(rects, insets.getBoundingRectsIgnoringVisibility(captionBar()));
+    }
 }
diff --git a/core/tests/coretests/src/android/view/WindowManagerTests.java b/core/tests/coretests/src/android/view/WindowManagerTests.java
index c5a9d48..211d768 100644
--- a/core/tests/coretests/src/android/view/WindowManagerTests.java
+++ b/core/tests/coretests/src/android/view/WindowManagerTests.java
@@ -25,8 +25,8 @@
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/view/WindowMetricsTest.java b/core/tests/coretests/src/android/view/WindowMetricsTest.java
index 39ea8af..f3ddfa6 100644
--- a/core/tests/coretests/src/android/view/WindowMetricsTest.java
+++ b/core/tests/coretests/src/android/view/WindowMetricsTest.java
@@ -27,9 +27,9 @@
 import android.os.Handler;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index 3d4918b..2d82d23 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@
     // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
     // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
     // and assertAccessibilityNodeInfoCleared in that class.
-    private static final int NUM_MARSHALLED_PROPERTIES = 43;
+    private static final int NUM_MARSHALLED_PROPERTIES = 44;
 
     /**
      * The number of properties that are purposely not marshalled
diff --git a/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java b/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java
index 3147eac..8db13c8 100644
--- a/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java
+++ b/core/tests/coretests/src/android/window/SystemPerformanceHinterTests.java
@@ -30,8 +30,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -42,14 +42,16 @@
 import android.view.SurfaceControl;
 
 import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.HashMap;
 
@@ -63,6 +65,9 @@
 @RunWith(AndroidJUnit4.class)
 public class SystemPerformanceHinterTests {
 
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     private static final int DEFAULT_DISPLAY_ID = android.view.Display.DEFAULT_DISPLAY;
     private static final int SECONDARY_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1;
     private static final int NO_ROOT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2;
@@ -83,8 +88,6 @@
 
     @Before
     public void setUpOnce() {
-        MockitoAnnotations.initMocks(this);
-
         mDefaultDisplayRoot = new SurfaceControl();
         mSecondaryDisplayRoot = new SurfaceControl();
         mRootProvider = new SystemPerformanceHinterTests.RootProvider();
diff --git a/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java b/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java
index 2dadb20..4589607 100644
--- a/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java
+++ b/core/tests/coretests/src/android/window/TaskFpsCallbackTest.java
@@ -24,9 +24,9 @@
 import android.platform.test.annotations.Presubmit;
 import android.view.WindowManager;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
index 30c0f2b..1f60b31 100644
--- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
@@ -34,14 +34,16 @@
 import android.os.Binder;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 /**
  * Tests for {@link WindowContextController}
@@ -56,6 +58,10 @@
 @SmallTest
 @Presubmit
 public class WindowContextControllerTest {
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     private WindowContextController mController;
     @Mock
     private WindowTokenClientController mWindowTokenClientController;
@@ -64,7 +70,6 @@
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
         mController = spy(new WindowContextController(mMockToken));
         doReturn(mWindowTokenClientController).when(mController).getWindowTokenClientController();
         doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt(), anyBoolean());
diff --git a/core/tests/coretests/src/android/window/WindowContextTest.java b/core/tests/coretests/src/android/window/WindowContextTest.java
index b2a4044..f1fbd55 100644
--- a/core/tests/coretests/src/android/window/WindowContextTest.java
+++ b/core/tests/coretests/src/android/window/WindowContextTest.java
@@ -53,10 +53,10 @@
 import android.view.WindowManagerImpl;
 
 import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.coretests.R;
 
diff --git a/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java b/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java
index 7cbb6b4..accc020 100644
--- a/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java
+++ b/core/tests/coretests/src/android/window/WindowMetricsHelperTest.java
@@ -31,9 +31,9 @@
 import android.view.WindowInsets;
 import android.view.WindowMetrics;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.window.flags.Flags;
 
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 9ae96a0..d153edd 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -44,18 +44,20 @@
 import android.view.ImeBackAnimationController;
 import android.view.MotionEvent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -70,6 +72,10 @@
 @SmallTest
 @Presubmit
 public class WindowOnBackInvokedDispatcherTest {
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     @Mock
     private IWindowSession mWindowSession;
     @Mock
@@ -106,8 +112,6 @@
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
         doReturn(true).when(mApplicationInfo).isOnBackInvokedCallbackEnabled();
         doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
 
diff --git a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
index a21c917..a3725af 100644
--- a/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java
@@ -39,9 +39,11 @@
 import androidx.test.filters.SmallTest;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 /**
  * Tests for {@link WindowTokenClientController}.
@@ -53,6 +55,9 @@
 @Presubmit
 public class WindowTokenClientControllerTest {
 
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
     @Mock
     private IWindowManager mWindowManagerService;
     @Mock
@@ -67,7 +72,6 @@
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
         mController = spy(WindowTokenClientController.createInstanceForTesting());
         doReturn(mWindowManagerService).when(mController).getWindowManagerService();
         mWindowContextInfo = new WindowContextInfo(mConfiguration, DEFAULT_DISPLAY);
diff --git a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java
index 9292f66..aa4c28a 100644
--- a/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java
+++ b/core/tests/coretests/src/android/window/flags/WindowFlagsTest.java
@@ -20,8 +20,8 @@
 
 import android.platform.test.annotations.Presubmit;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java
new file mode 100644
index 0000000..537dd69
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.policy;
+
+import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_DEFAULT;
+import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_SLEEP_ON_FOLD;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link FoldLockSettingsObserver}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FoldLockSettingsObserverTest {
+    @Mock
+    private Context mContext;
+    @Mock
+    private Handler mHandler;
+    @Mock
+    private ContentResolver mContentResolver;
+
+    private FoldLockSettingsObserver mFoldLockSettingsObserver;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFoldLockSettingsObserver =
+                spy(new FoldLockSettingsObserver(mHandler, mContext));
+
+        doReturn(mContentResolver).when(mContext).getContentResolver();
+        doReturn(SETTING_VALUE_DEFAULT).when(mFoldLockSettingsObserver).request();
+
+        mFoldLockSettingsObserver.register();
+    }
+
+    @Test
+    public void shouldRegister() {
+        doReturn(mContentResolver).when(mContext).getContentResolver();
+
+        mFoldLockSettingsObserver.register();
+
+        verify(mContentResolver).registerContentObserver(
+                Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR),
+                false /*notifyForDescendants */,
+                mFoldLockSettingsObserver,
+                UserHandle.USER_ALL
+        );
+    }
+
+    @Test
+    public void shouldUnregister() {
+        mFoldLockSettingsObserver.unregister();
+
+        verify(mContentResolver).unregisterContentObserver(mFoldLockSettingsObserver);
+    }
+
+    @Test
+    public void shouldCacheNewValue() {
+        // Reset the mock's behavior and call count to zero.
+        reset(mFoldLockSettingsObserver);
+        doReturn(SETTING_VALUE_SLEEP_ON_FOLD).when(mFoldLockSettingsObserver).request();
+
+        // Setting is DEFAULT at first.
+        assertEquals(SETTING_VALUE_DEFAULT, mFoldLockSettingsObserver.mFoldLockSetting);
+
+        // Cache new setting.
+        mFoldLockSettingsObserver.requestAndCacheFoldLockSetting();
+
+        // Check that setter was called once and change went through properly.
+        verify(mFoldLockSettingsObserver).setCurrentFoldSetting(anyString());
+        assertTrue(mFoldLockSettingsObserver.isSleepOnFold());
+    }
+}
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index 7c7cb18..9887c27 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
@@ -55,9 +55,9 @@
 import com.sun.tools.javac.code.Symbol;
 import com.sun.tools.javac.code.Symbol.ClassSymbol;
 import com.sun.tools.javac.code.Symbol.MethodSymbol;
-import com.sun.tools.javac.code.Symbol.VarSymbol;
 import com.sun.tools.javac.code.Type;
 import com.sun.tools.javac.code.Type.ClassType;
+import com.sun.tools.javac.tree.JCTree.JCNewClass;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -67,7 +67,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
 import java.util.regex.Pattern;
 
 import javax.lang.model.element.Name;
@@ -125,6 +124,12 @@
             instanceMethod()
                     .onDescendantOf("android.content.Context")
                     .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$")));
+    private static final Matcher<ExpressionTree> SEND_BROADCAST_AS_USER =
+            methodInvocation(
+                    instanceMethod()
+                            .onDescendantOf("android.content.Context")
+                            .withNameMatching(
+                                    Pattern.compile("^send(Ordered|Sticky)?Broadcast.*AsUser.*$")));
     private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation(
             instanceMethod()
                     .onDescendantOf("android.app.PendingIntent")
@@ -306,18 +311,6 @@
         }
     }
 
-    private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree,
-            Predicate<String> paramName) {
-        final MethodSymbol sym = ASTHelpers.getSymbol(tree);
-        final List<VarSymbol> params = sym.getParameters();
-        for (int i = 0; i < params.size(); i++) {
-            if (paramName.test(params.get(i).name.toString())) {
-                return tree.getArguments().get(i);
-            }
-        }
-        return null;
-    }
-
     private static Name resolveName(ExpressionTree tree) {
         if (tree instanceof IdentifierTree) {
             return ((IdentifierTree) tree).getName();
@@ -345,76 +338,85 @@
 
     private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission(
             MethodInvocationTree methodTree, VisitorState state) {
-        final ExpressionTree arg = findArgumentByParameterName(methodTree,
-                (name) -> name.toLowerCase().contains("intent"));
-        if (arg instanceof IdentifierTree) {
-            final Name argName = ((IdentifierTree) arg).getName();
-            final MethodTree method = state.findEnclosing(MethodTree.class);
-            final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
-            method.accept(new TreeScanner<Void, Void>() {
-                private ParsedRequiresPermission last;
+        if (methodTree.getArguments().size() < 1) {
+            return null;
+        }
+        final ExpressionTree arg = methodTree.getArguments().get(0);
+        if (!(arg instanceof IdentifierTree)) {
+            return null;
+        }
+        final Name argName = ((IdentifierTree) arg).getName();
+        final MethodTree method = state.findEnclosing(MethodTree.class);
+        final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
+        method.accept(new TreeScanner<Void, Void>() {
+            private ParsedRequiresPermission mLast;
 
-                @Override
-                public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
-                    if (Objects.equal(methodTree, tree)) {
-                        res.set(last);
-                    } else {
-                        final Name name = resolveName(tree.getMethodSelect());
-                        if (Objects.equal(argName, name)
-                                && INTENT_SET_ACTION.matches(tree, state)) {
-                            last = parseIntentAction(tree);
+            @Override
+            public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
+                if (Objects.equal(methodTree, tree)) {
+                    res.set(mLast);
+                } else {
+                    final Name name = resolveName(tree.getMethodSelect());
+                    if (Objects.equal(argName, name) && INTENT_SET_ACTION.matches(tree, state)) {
+                        mLast = parseIntentAction(tree);
+                    } else if (name == null && tree.getMethodSelect() instanceof MemberSelectTree) {
+                        ExpressionTree innerTree =
+                                ((MemberSelectTree) tree.getMethodSelect()).getExpression();
+                        if (innerTree instanceof JCNewClass) {
+                            mLast = parseIntentAction((NewClassTree) innerTree);
                         }
                     }
-                    return super.visitMethodInvocation(tree, param);
                 }
+                return super.visitMethodInvocation(tree, param);
+            }
 
-                @Override
-                public Void visitAssignment(AssignmentTree tree, Void param) {
-                    final Name name = resolveName(tree.getVariable());
-                    final Tree init = tree.getExpression();
-                    if (Objects.equal(argName, name)
-                            && init instanceof NewClassTree) {
-                        last = parseIntentAction((NewClassTree) init);
-                    }
-                    return super.visitAssignment(tree, param);
+            @Override
+            public Void visitAssignment(AssignmentTree tree, Void param) {
+                final Name name = resolveName(tree.getVariable());
+                final Tree init = tree.getExpression();
+                if (Objects.equal(argName, name) && init instanceof NewClassTree) {
+                    mLast = parseIntentAction((NewClassTree) init);
                 }
+                return super.visitAssignment(tree, param);
+            }
 
-                @Override
-                public Void visitVariable(VariableTree tree, Void param) {
-                    final Name name = tree.getName();
-                    final ExpressionTree init = tree.getInitializer();
-                    if (Objects.equal(argName, name)
-                            && init instanceof NewClassTree) {
-                        last = parseIntentAction((NewClassTree) init);
-                    }
-                    return super.visitVariable(tree, param);
+            @Override
+            public Void visitVariable(VariableTree tree, Void param) {
+                final Name name = tree.getName();
+                final ExpressionTree init = tree.getInitializer();
+                if (Objects.equal(argName, name) && init instanceof NewClassTree) {
+                    mLast = parseIntentAction((NewClassTree) init);
                 }
-            }, null);
-            return res.get();
-        }
-        return null;
+                return super.visitVariable(tree, param);
+            }
+        }, null);
+        return res.get();
     }
 
     private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission(
             MethodInvocationTree tree, VisitorState state) {
-        final ExpressionTree arg = findArgumentByParameterName(tree,
-                (name) -> name.toLowerCase().contains("permission"));
         final ParsedRequiresPermission res = new ParsedRequiresPermission();
-        if (arg != null) {
-            arg.accept(new TreeScanner<Void, Void>() {
-                @Override
-                public Void visitIdentifier(IdentifierTree tree, Void param) {
-                    res.addConstValue(tree);
-                    return super.visitIdentifier(tree, param);
-                }
-
-                @Override
-                public Void visitMemberSelect(MemberSelectTree tree, Void param) {
-                    res.addConstValue(tree);
-                    return super.visitMemberSelect(tree, param);
-                }
-            }, null);
+        int permission_position = 1;
+        if (SEND_BROADCAST_AS_USER.matches(tree, state)) {
+            permission_position = 2;
         }
+        if (tree.getArguments().size() < permission_position + 1) {
+            return res;
+        }
+        final ExpressionTree arg = tree.getArguments().get(permission_position);
+        arg.accept(new TreeScanner<Void, Void>() {
+            @Override
+            public Void visitIdentifier(IdentifierTree tree, Void param) {
+                res.addConstValue(tree);
+                return super.visitIdentifier(tree, param);
+            }
+
+            @Override
+            public Void visitMemberSelect(MemberSelectTree tree, Void param) {
+                res.addConstValue(tree);
+                return super.visitMemberSelect(tree, param);
+            }
+        }, null);
         return res;
     }
 
diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
index e53372d..05fde7c 100644
--- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
+++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java
@@ -412,6 +412,19 @@
                         "      context.sendBroadcast(intent);",
                         "    }",
                         "  }",
+                        "  public void exampleWithChainedMethod(Context context) {",
+                        "    Intent intent = new Intent(FooManager.ACTION_RED)",
+                        "            .putExtra(\"foo\", 42);",
+                        "    context.sendBroadcast(intent, FooManager.PERMISSION_RED);",
+                        "    context.sendBroadcastWithMultiplePermissions(intent,",
+                        "        new String[] { FooManager.PERMISSION_RED });",
+                        "  }",
+                        "  public void exampleWithAsUser(Context context) {",
+                        "    Intent intent = new Intent(FooManager.ACTION_RED);",
+                        "    context.sendBroadcastAsUser(intent, 42, FooManager.PERMISSION_RED);",
+                        "    context.sendBroadcastAsUserMultiplePermissions(intent, 42,",
+                        "        new String[] { FooManager.PERMISSION_RED });",
+                        "  }",
                         "}")
                 .doTest();
     }
diff --git a/errorprone/tests/res/android/content/Context.java b/errorprone/tests/res/android/content/Context.java
index efc4fb1..9d622ff 100644
--- a/errorprone/tests/res/android/content/Context.java
+++ b/errorprone/tests/res/android/content/Context.java
@@ -36,4 +36,15 @@
     public void sendBroadcastWithMultiplePermissions(Intent intent, String[] receiverPermissions) {
         throw new UnsupportedOperationException();
     }
+
+    /* Fake user type for test purposes */
+    public void sendBroadcastAsUser(Intent intent, int user, String receiverPermission) {
+        throw new UnsupportedOperationException();
+    }
+
+    /* Fake user type for test purposes */
+    public void sendBroadcastAsUserMultiplePermissions(
+            Intent intent, int user, String[] receiverPermissions) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/errorprone/tests/res/android/content/Intent.java b/errorprone/tests/res/android/content/Intent.java
index 288396e..7ccea78 100644
--- a/errorprone/tests/res/android/content/Intent.java
+++ b/errorprone/tests/res/android/content/Intent.java
@@ -24,4 +24,8 @@
     public Intent setAction(String action) {
         throw new UnsupportedOperationException();
     }
+
+    public Intent putExtra(String extra, int value) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index df95a91..b83931f 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -35,6 +35,7 @@
 import android.graphics.fonts.FontVariationAxis;
 import android.os.Build;
 import android.os.LocaleList;
+import android.text.ClientFlags;
 import android.text.GraphicsOperations;
 import android.text.SpannableString;
 import android.text.SpannedString;
@@ -1540,8 +1541,21 @@
      * @return         typeface
      */
     public Typeface setTypeface(Typeface typeface) {
+        return setTypefaceInternal(typeface, true);
+    }
+
+    private Typeface setTypefaceInternal(Typeface typeface, boolean clearFontVariationSettings) {
         final long typefaceNative = typeface == null ? 0 : typeface.native_instance;
         nSetTypeface(mNativePaint, typefaceNative);
+
+        if (ClientFlags.clearFontVariationSettings()) {
+            if (clearFontVariationSettings && !Objects.equals(mTypeface, typeface)) {
+                // We cannot call setFontVariationSetting with empty string or null because it calls
+                // setTypeface method. To avoid recursive setTypeface call, manually resetting
+                // mFontVariationSettings.
+                mFontVariationSettings = null;
+            }
+        }
         mTypeface = typeface;
         return typeface;
     }
@@ -2037,6 +2051,14 @@
      * </li>
      * </ul>
      *
+     * Note: This method replaces the Typeface previously set to this instance.
+     * Until API {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, any caller of
+     * {@link #setTypeface(Typeface)} should call this method with empty settings, then call
+     * {@link #setTypeface(Typeface)}, then call this method with preferred variation settings.
+     * The device API more than {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, the
+     * {@link #setTypeface(Typeface)} method clears font variation settings. So caller of
+     * {@link #setTypeface(Typeface)} should call this method again for applying variation settings.
+     *
      * @param fontVariationSettings font variation settings. You can pass null or empty string as
      *                              no variation settings.
      *
@@ -2059,8 +2081,8 @@
 
         if (settings == null || settings.length() == 0) {
             mFontVariationSettings = null;
-            setTypeface(Typeface.createFromTypefaceWithVariation(mTypeface,
-                      Collections.emptyList()));
+            setTypefaceInternal(Typeface.createFromTypefaceWithVariation(mTypeface,
+                      Collections.emptyList()), false);
             return true;
         }
 
@@ -2078,7 +2100,8 @@
             return false;
         }
         mFontVariationSettings = settings;
-        setTypeface(Typeface.createFromTypefaceWithVariation(targetTypeface, filteredAxes));
+        setTypefaceInternal(Typeface.createFromTypefaceWithVariation(targetTypeface, filteredAxes),
+                false);
         return true;
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index ea60b15..f1e7ef5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -1359,4 +1359,16 @@
         return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration,
                 windowLayoutInfo);
     }
+
+    @VisibleForTesting
+    @NonNull
+    static String positionToString(@ContainerPosition int position) {
+        return switch (position) {
+            case CONTAINER_POSITION_LEFT -> "left";
+            case CONTAINER_POSITION_TOP -> "top";
+            case CONTAINER_POSITION_RIGHT -> "right";
+            case CONTAINER_POSITION_BOTTOM -> "bottom";
+            default -> "Unknown position:" + position;
+        };
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index 61ea51a..139ddda 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -32,6 +32,7 @@
     ],
 
     static_libs: [
+        "TestParameterInjector",
         "androidx.window.extensions",
         "androidx.window.extensions.core_core",
         "junit",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index 3257502..1c4c887 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -35,6 +35,7 @@
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
 import static androidx.window.extensions.embedding.SplitPresenter.getOverlayPosition;
+import static androidx.window.extensions.embedding.SplitPresenter.positionToString;
 import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds;
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
@@ -78,7 +79,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
@@ -86,6 +86,9 @@
 
 import com.android.window.flags.Flags;
 
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -108,7 +111,7 @@
 @SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(TestParameterInjector.class)
 public class OverlayPresentationTest {
     @Rule
     public MockitoRule rule = MockitoJUnit.rule();
@@ -875,57 +878,70 @@
                 eq(overlayContainer.getTaskFragmentToken()), eq(activityToken));
     }
 
-    // TODO(b/243518738): Rewrite with TestParameter.
     @Test
-    public void testGetOverlayPosition() {
-        assertWithMessage("It must be position left for left overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left,
-                        TASK_BOUNDS.top,
-                        TASK_BOUNDS.right / 2,
-                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT);
-        assertWithMessage("It must be position left for shrunk left overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left,
-                        TASK_BOUNDS.top + 20,
-                        TASK_BOUNDS.right / 2,
-                        TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_LEFT);
-        assertWithMessage("It must be position left for top overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left,
-                        TASK_BOUNDS.top,
-                        TASK_BOUNDS.right,
-                        TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP);
-        assertWithMessage("It must be position left for shrunk top overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left + 20,
-                        TASK_BOUNDS.top,
-                        TASK_BOUNDS.right - 20,
-                        TASK_BOUNDS.bottom / 2), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_TOP);
-        assertWithMessage("It must be position left for right overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.right / 2,
-                        TASK_BOUNDS.top,
-                        TASK_BOUNDS.right,
-                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT);
-        assertWithMessage("It must be position left for shrunk right overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.right / 2,
-                        TASK_BOUNDS.top + 20,
-                        TASK_BOUNDS.right,
-                        TASK_BOUNDS.bottom - 20), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_RIGHT);
-        assertWithMessage("It must be position left for bottom overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left,
-                        TASK_BOUNDS.bottom / 2,
-                        TASK_BOUNDS.right,
-                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM);
-        assertWithMessage("It must be position left for shrunk bottom overlay.")
-                .that(getOverlayPosition(new Rect(
-                        TASK_BOUNDS.left + 20,
-                        TASK_BOUNDS.bottom / 20,
-                        TASK_BOUNDS.right - 20,
-                        TASK_BOUNDS.bottom), TASK_BOUNDS)).isEqualTo(CONTAINER_POSITION_BOTTOM);
+    public void testGetOverlayPosition(@TestParameter OverlayPositionTestParams params) {
+        final Rect taskBounds = new Rect(TASK_BOUNDS);
+        final Rect overlayBounds = params.toOverlayBounds();
+        final int overlayPosition = getOverlayPosition(overlayBounds, taskBounds);
+
+        assertWithMessage("The overlay position must be "
+                + positionToString(params.mPosition) + ", but is "
+                + positionToString(overlayPosition)
+                + ", parent bounds=" + taskBounds + ", overlay bounds=" + overlayBounds)
+                .that(overlayPosition).isEqualTo(params.mPosition);
+    }
+
+    private enum OverlayPositionTestParams {
+        LEFT_OVERLAY(CONTAINER_POSITION_LEFT, false /* shouldBeShrunk */),
+        LEFT_SHRUNK_OVERLAY(CONTAINER_POSITION_LEFT, true  /* shouldBeShrunk */),
+        TOP_OVERLAY(CONTAINER_POSITION_TOP, false /* shouldBeShrunk */),
+        TOP_SHRUNK_OVERLAY(CONTAINER_POSITION_TOP, true  /* shouldBeShrunk */),
+        RIGHT_OVERLAY(CONTAINER_POSITION_RIGHT, false /* shouldBeShrunk */),
+        RIGHT_SHRUNK_OVERLAY(CONTAINER_POSITION_RIGHT, true /* shouldBeShrunk */),
+        BOTTOM_OVERLAY(CONTAINER_POSITION_BOTTOM, false /* shouldBeShrunk */),
+        BOTTOM_SHRUNK_OVERLAY(CONTAINER_POSITION_BOTTOM, true /* shouldBeShrunk */);
+
+        @SplitPresenter.ContainerPosition
+        private final int mPosition;
+
+        private final boolean mShouldBeShrunk;
+
+        OverlayPositionTestParams(
+                @SplitPresenter.ContainerPosition int position, boolean shouldBeShrunk) {
+            mPosition = position;
+            mShouldBeShrunk = shouldBeShrunk;
+        }
+
+        @NonNull
+        private Rect toOverlayBounds() {
+            Rect r = new Rect(TASK_BOUNDS);
+            final int offset = mShouldBeShrunk ? 20 : 0;
+            switch (mPosition) {
+                case CONTAINER_POSITION_LEFT:
+                    r.top += offset;
+                    r.right /= 2;
+                    r.bottom -= offset;
+                    break;
+                case CONTAINER_POSITION_TOP:
+                    r.left += offset;
+                    r.right -= offset;
+                    r.bottom /= 2;
+                    break;
+                case CONTAINER_POSITION_RIGHT:
+                    r.left = r.right / 2;
+                    r.top += offset;
+                    r.bottom -= offset;
+                    break;
+                case CONTAINER_POSITION_BOTTOM:
+                    r.left += offset;
+                    r.right -= offset;
+                    r.top = r.bottom / 2;
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid position: " + mPosition);
+            }
+            return r;
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index e6cb3a0..5135e9e 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -239,6 +239,9 @@
         "wmshell.protolog.json.gz",
         "wmshell.protolog.pb",
     ],
+    flags_packages: [
+        "com_android_wm_shell_flags",
+    ],
     kotlincflags: ["-Xjvm-default=all"],
     manifest: "AndroidManifest.xml",
     plugins: ["dagger2-compiler"],
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 52ae93f..bbbc23e 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -34,6 +34,7 @@
 
         <activity
             android:name=".bubbles.shortcut.CreateBubbleShortcutActivity"
+            android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
             android:exported="true"
             android:excludeFromRecents="true"
             android:theme="@android:style/Theme.NoDisplay"
@@ -47,6 +48,7 @@
 
         <activity
             android:name=".bubbles.shortcut.ShowBubblesActivity"
+            android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles"
             android:exported="true"
             android:excludeFromRecents="true"
             android:theme="@android:style/Theme.NoDisplay" >
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
index 235b9bf..fc3dc14 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
@@ -168,6 +168,16 @@
         }
     }
 
+    /** Whether any animation is currently running. */
+    @JvmStatic
+    fun isAnyAnimationRunning(): Boolean {
+        for (target in allAnimatedObjects) {
+            val animator = PhysicsAnimator.getInstance(target)
+            if (animator.isRunning()) return true
+        }
+        return false
+    }
+
     /**
      * Blocks the calling thread until the first animation frame in which predicate returns true. If
      * the given object isn't animating, returns without blocking.
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 bb239ad..a9fdea3 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
@@ -114,6 +114,7 @@
 
     @Nullable
     private BackNavigationInfo mBackNavigationInfo;
+    private boolean mReceivedNullNavigationInfo = false;
     private final IActivityTaskManager mActivityTaskManager;
     private final Context mContext;
     private final ContentResolver mContentResolver;
@@ -430,7 +431,7 @@
         mThresholdCrossed = true;
         // There was no focus window when calling startBackNavigation, still pilfer pointers so
         // the next focus window won't receive motion events.
-        if (mBackNavigationInfo == null) {
+        if (mBackNavigationInfo == null && mReceivedNullNavigationInfo) {
             tryPilferPointers();
             return;
         }
@@ -553,6 +554,7 @@
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
         if (backNavigationInfo == null) {
             ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
+            mReceivedNullNavigationInfo = true;
             cancelLatencyTracking();
             tryPilferPointers();
             return;
@@ -909,6 +911,7 @@
         mPointersPilfered = false;
         mShellBackAnimationRegistry.resetDefaultCrossActivity();
         cancelLatencyTracking();
+        mReceivedNullNavigationInfo = false;
         if (mBackNavigationInfo != null) {
             mPreviousNavigationType = mBackNavigationInfo.getType();
             mBackNavigationInfo.onBackNavigationFinished(triggerBack);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index f7a5c27..d4d9d00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.bubbles;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -225,8 +225,7 @@
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
                     options.setPendingIntentBackgroundActivityStartMode(
-                            MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-                    options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                            MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
                     Intent fillInIntent = new Intent();
                     // Apply flags to make behaviour match documentLaunchMode=always.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index c79d9c4..5e2141a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -15,7 +15,7 @@
  */
 package com.android.wm.shell.bubbles;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -103,8 +103,7 @@
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
                     options.setPendingIntentBackgroundActivityStartMode(
-                            MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-                    options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                            MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
                     Intent fillInIntent = new Intent();
                     // Apply flags to make behaviour match documentLaunchMode=always.
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 e713af6..80f6a63 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
@@ -211,6 +211,7 @@
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
             @ShellMainThread Choreographer mainChoreographer,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
             IWindowManager windowManager,
             ShellCommandHandler shellCommandHandler,
@@ -229,6 +230,7 @@
                     mainExecutor,
                     mainHandler,
                     mainChoreographer,
+                    bgExecutor,
                     shellInit,
                     shellCommandHandler,
                     windowManager,
@@ -246,6 +248,7 @@
                 context,
                 mainHandler,
                 mainExecutor,
+                bgExecutor,
                 mainChoreographer,
                 windowManager,
                 shellInit,
@@ -366,13 +369,14 @@
             Optional<WindowDecorViewModel> windowDecorViewModel,
             Optional<DesktopTasksController> desktopTasksController,
             MultiInstanceHelper multiInstanceHelper,
-            @ShellMainThread ShellExecutor mainExecutor) {
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler) {
         return new SplitScreenController(context, shellInit, shellCommandHandler, shellController,
                 shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController,
                 displayImeController, displayInsetsController, dragAndDropController, transitions,
                 transactionPool, iconProvider, recentTasks, launchAdjacentController,
                 windowDecorViewModel, desktopTasksController, null /* stageCoordinator */,
-                multiInstanceHelper, mainExecutor);
+                multiInstanceHelper, mainExecutor, mainHandler);
     }
 
     //
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 1a9c304..037fbb2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -206,12 +206,13 @@
     @WMSingleton
     @Provides
     static PipMotionHelper providePipMotionHelper(Context context,
+            @ShellMainThread ShellExecutor mainExecutor,
             PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer,
             PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm,
             PipTransitionController pipTransitionController,
             FloatingContentCoordinator floatingContentCoordinator,
             Optional<PipPerfHintController> pipPerfHintControllerOptional) {
-        return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer,
+        return new PipMotionHelper(context, mainExecutor, pipBoundsState, pipTaskOrganizer,
                 menuController, pipSnapAlgorithm, pipTransitionController,
                 floatingContentCoordinator, pipPerfHintControllerOptional);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index 066b5ad..73aa7ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -347,7 +347,7 @@
             else -> {
                 ProtoLog.w(
                     WM_SHELL_DESKTOP_MODE,
-                    "Unknown enter reason for transition type ${transitionInfo.type}",
+                    "Unknown enter reason for transition type: %s",
                     transitionInfo.type
                 )
                 EnterReason.UNKNOWN_ENTER
@@ -368,7 +368,7 @@
             else -> {
                 ProtoLog.w(
                     WM_SHELL_DESKTOP_MODE,
-                    "Unknown exit reason for transition type ${transitionInfo.type}",
+                    "Unknown exit reason for transition type: %s",
                     transitionInfo.type
                 )
                 ExitReason.UNKNOWN_EXIT
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 1bf1259..da212e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -66,7 +66,7 @@
                         idealSize
                     }
                 } else {
-                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                    maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                 }
             }
             ORIENTATION_PORTRAIT -> {
@@ -85,13 +85,13 @@
                 } else {
                     if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
                         // Apply custom app width and calculate maximum size
-                        maximumSizeMaintainingAspectRatio(
+                        maximizeSizeGivenAspectRatio(
                             taskInfo,
                             Size(customPortraitWidthForLandscapeApp, idealSize.height),
                             appAspectRatio
                         )
                     } else {
-                        maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                        maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                     }
                 }
             }
@@ -107,7 +107,7 @@
  * Calculates the largest size that can fit in a given area while maintaining a specific aspect
  * ratio.
  */
-fun maximumSizeMaintainingAspectRatio(
+fun maximizeSizeGivenAspectRatio(
     taskInfo: RunningTaskInfo,
     targetArea: Size,
     aspectRatio: Float
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 9a1a8a2..de901b5 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
@@ -69,6 +69,7 @@
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
+import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
 import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
 import com.android.wm.shell.draganddrop.DragAndDropController
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -671,7 +672,7 @@
             } else {
                 // if non-resizable then calculate max bounds according to aspect ratio
                 val activityAspectRatio = calculateAspectRatio(taskInfo)
-                val newSize = maximumSizeMaintainingAspectRatio(taskInfo,
+                val newSize = maximizeSizeGivenAspectRatio(taskInfo,
                     Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
                 val newBounds = centerInArea(
                     newSize, stableBounds, stableBounds.left, stableBounds.top)
@@ -818,9 +819,8 @@
         val intent = Intent(context, DesktopWallpaperActivity::class.java)
         val options =
             ActivityOptions.makeBasic().apply {
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
                 pendingIntentBackgroundActivityStartMode =
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
             }
         val pendingIntent =
             PendingIntent.getActivity(
@@ -983,6 +983,7 @@
             ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked")
             return null
         }
+        val wct = WindowContainerTransaction()
         if (!isDesktopModeShowing(task.displayId)) {
             ProtoLog.d(
                 WM_SHELL_DESKTOP_MODE,
@@ -990,12 +991,17 @@
                     " taskId=%d",
                 task.taskId
             )
-            return WindowContainerTransaction().also { wct ->
-                bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
-                wct.reorder(task.token, true)
+            // We are outside of desktop mode and already existing desktop task is being launched.
+            // We should make this task go to fullscreen instead of freeform. Note that this means
+            // any re-launch of a freeform window outside of desktop will be in fullscreen.
+            if (desktopModeTaskRepository.isActiveTask(task.taskId)) {
+                addMoveToFullscreenChanges(wct, task)
+                return wct
             }
+            bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
+            wct.reorder(task.token, true)
+            return wct
         }
-        val wct = WindowContainerTransaction()
         if (useDesktopOverrideDensity()) {
             wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
         }
@@ -1074,7 +1080,6 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
-        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode =
@@ -1084,9 +1089,6 @@
             } else {
                 WINDOWING_MODE_FREEFORM
             }
-        if (Flags.enableWindowingDynamicInitialBounds()) {
-            wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
-        }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.reorder(taskInfo.token, true /* onTop */)
         if (useDesktopOverrideDensity()) {
@@ -1333,33 +1335,36 @@
      *
      * @param taskInfo the task being dragged.
      * @param y height of drag, to be checked against status bar height.
+     * @return the [IndicatorType] used for the resulting transition
      */
     fun onDragPositioningEndThroughStatusBar(
         inputCoordinates: PointF,
         taskInfo: RunningTaskInfo,
-    ) {
-        val indicator = getVisualIndicator() ?: return
+    ): IndicatorType {
+        val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR
         val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
         when (indicatorType) {
-            DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
-                val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+            IndicatorType.TO_DESKTOP_INDICATOR -> {
+                val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
+                    ?: return IndicatorType.NO_INDICATOR
                 if (Flags.enableWindowingDynamicInitialBounds()) {
                     finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
                 } else {
                     finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
                 }
             }
-            DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
-            DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+            IndicatorType.NO_INDICATOR,
+            IndicatorType.TO_FULLSCREEN_INDICATOR -> {
                 cancelDragToDesktop(taskInfo)
             }
-            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
+            IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
                 requestSplit(taskInfo, leftOrTop = true)
             }
-            DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+            IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
                 requestSplit(taskInfo, leftOrTop = false)
             }
         }
+        return indicatorType
     }
 
     /** Update the exclusion region for a specified task */
@@ -1420,7 +1425,6 @@
                 setPendingIntentBackgroundActivityStartMode(
                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
                 )
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
             }
         val wct = WindowContainerTransaction()
         wct.sendPendingIntent(launchIntent, null, opts.toBundle())
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 95fe8b6..7e03624 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.draganddrop;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -280,8 +280,7 @@
         baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true);
         // Put BAL flags to avoid activity start aborted.
         baseActivityOpts.setPendingIntentBackgroundActivityStartMode(
-                MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+                MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
         final Bundle opts = baseActivityOpts.toBundle();
         if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) {
             opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS));
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 a749019..b27c428 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
@@ -16,10 +16,12 @@
 
 package com.android.wm.shell.pip;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 
 import com.android.wm.shell.shared.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -69,9 +71,10 @@
     default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { }
 
     /**
-     * @return {@link PipTransitionController} instance.
+     * Register {@link PipTransitionController.PipTransitionCallback} to listen on PiP transition
+     * started / finished callbacks.
      */
-    default PipTransitionController getPipTransitionController() {
-        return null;
-    }
+    default void registerPipTransitionCallback(
+            @NonNull PipTransitionController.PipTransitionCallback callback,
+            @NonNull Executor executor) { }
 }
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 a8346a9..852382d 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
@@ -230,6 +230,7 @@
 
     /**
      * Quietly cancel the animator by removing the listeners first.
+     * TODO(b/275003573): deprecate this, cancelling without the proper callbacks is problematic.
      */
     static void quietCancel(@NonNull ValueAnimator animator) {
         animator.removeAllUpdateListeners();
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 8d63ff2..723a531 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
@@ -423,7 +423,8 @@
             });
             mPipTransitionController.setPipOrganizer(this);
             displayController.addDisplayWindowListener(this);
-            pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
+            pipTransitionController.registerPipTransitionCallback(
+                    mPipTransitionCallback, mMainExecutor);
         }
     }
 
@@ -495,7 +496,9 @@
         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                 "startSwipePipToHome: %s, state=%s", componentName, mPipTransitionState);
         mPipTransitionState.setInSwipePipToHomeTransition(true);
-        sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+        if (!ENABLE_SHELL_TRANSITIONS) {
+            sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+        }
         setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo);
         return mPipBoundsAlgorithm.getEntryDestinationBounds();
     }
@@ -2023,7 +2026,7 @@
             removeContentOverlay(mPipOverlay, null /* callback */);
         }
         if (animator != null) {
-            PipAnimationController.quietCancel(animator);
+            animator.cancel();
             mPipAnimationController.resetAnimatorState();
         }
     }
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 e5633de..9bb9d86 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
@@ -1173,7 +1173,13 @@
                     .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE);
         }
 
-        final Rect sourceBounds = pipTaskInfo.configuration.windowConfiguration.getBounds();
+        sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+        // Both Shell and Launcher calculate their own "adjusted" source-rect-hint values based on
+        // appBounds being source bounds when entering PiP.
+        final Rect sourceBounds = swipePipToHomeOverlay == null
+                ? pipTaskInfo.configuration.windowConfiguration.getBounds()
+                : mPipOrganizer.mAppBounds;
+
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds,
                         destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index b1dd4f1..fc9e2be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -53,8 +53,9 @@
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
 
 /**
  * Responsible supplying PiP Transitions.
@@ -66,7 +67,7 @@
     protected final ShellTaskOrganizer mShellTaskOrganizer;
     protected final PipMenuController mPipMenuController;
     protected final Transitions mTransitions;
-    private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
+    private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>();
     protected PipTaskOrganizer mPipOrganizer;
     protected DefaultMixedHandler mMixedHandler;
 
@@ -183,16 +184,20 @@
     /**
      * Registers {@link PipTransitionCallback} to receive transition callbacks.
      */
-    public void registerPipTransitionCallback(PipTransitionCallback callback) {
-        mPipTransitionCallbacks.add(callback);
+    public void registerPipTransitionCallback(
+            @NonNull PipTransitionCallback callback, @NonNull Executor executor) {
+        mPipTransitionCallbacks.put(callback, executor);
     }
 
     protected void sendOnPipTransitionStarted(
             @PipAnimationController.TransitionDirection int direction) {
         final Rect pipBounds = mPipBoundsState.getBounds();
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionStarted(direction, pipBounds);
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "sendOnPipTransitionStarted direction=%d, bounds=%s", direction, pipBounds);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionStarted(direction, pipBounds));
         }
         if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) {
             try {
@@ -209,9 +214,12 @@
 
     protected void sendOnPipTransitionFinished(
             @PipAnimationController.TransitionDirection int direction) {
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionFinished(direction);
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "sendOnPipTransitionFinished direction=%d", direction);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionFinished(direction));
         }
         if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) {
             try {
@@ -228,9 +236,12 @@
 
     protected void sendOnPipTransitionCancelled(
             @PipAnimationController.TransitionDirection int direction) {
-        for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) {
-            final PipTransitionCallback callback = mPipTransitionCallbacks.get(i);
-            callback.onPipTransitionCanceled(direction);
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "sendOnPipTransitionCancelled direction=%d", direction);
+        for (Map.Entry<PipTransitionCallback, Executor> entry
+                : mPipTransitionCallbacks.entrySet()) {
+            entry.getValue().execute(
+                    () -> entry.getKey().onPipTransitionCanceled(direction));
         }
     }
 
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 26b7e58..0cb7e17 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -106,6 +106,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 /**
@@ -478,7 +479,7 @@
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
                 INPUT_CONSUMER_PIP, mMainExecutor);
-        mPipTransitionController.registerPipTransitionCallback(this);
+        mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor);
         mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> {
             mPipDisplayLayoutState.setDisplayId(displayId);
             onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
@@ -1220,8 +1221,11 @@
         }
 
         @Override
-        public PipTransitionController getPipTransitionController() {
-            return mPipTransitionController;
+        public void registerPipTransitionCallback(
+                PipTransitionController.PipTransitionCallback callback,
+                Executor executor) {
+            mMainExecutor.execute(() -> mPipTransitionController.registerPipTransitionCallback(
+                    callback, executor));
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index e8d6576..df3803d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.FloatProperties;
 import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.PipBoundsState;
@@ -47,6 +48,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
@@ -171,7 +173,9 @@
         public void onPipTransitionCanceled(int direction) {}
     };
 
-    public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState,
+    public PipMotionHelper(Context context,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @NonNull PipBoundsState pipBoundsState,
             PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController,
             PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -183,7 +187,7 @@
         mSnapAlgorithm = snapAlgorithm;
         mFloatingContentCoordinator = floatingContentCoordinator;
         mPipPerfHintController = pipPerfHintControllerOptional.orElse(null);
-        pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
+        pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback, mainExecutor);
         mResizePipUpdateListener = (target, values) -> {
             if (mPipBoundsState.getMotionBoundsState().isInMotion()) {
                 mPipTaskOrganizer.scheduleUserResizePip(getBounds(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 62c0944..0ed5079 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -257,7 +257,7 @@
     }
 
     private void onInit() {
-        mPipTransitionController.registerPipTransitionCallback(this);
+        mPipTransitionController.registerPipTransitionCallback(this, mMainExecutor);
 
         reloadResources();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java
new file mode 100644
index 0000000..8a9302b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java
@@ -0,0 +1,150 @@
+/*
+ * 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.pip2.animation;
+
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Animator that handles bounds animations for entering / exiting PIP.
+ */
+public class PipEnterExitAnimator extends ValueAnimator
+        implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
+    @IntDef(prefix = {"BOUNDS_"}, value = {
+            BOUNDS_ENTER,
+            BOUNDS_EXIT
+    })
+
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BOUNDS {}
+
+    public static final int BOUNDS_ENTER = 0;
+    public static final int BOUNDS_EXIT = 1;
+
+    @NonNull private final SurfaceControl mLeash;
+    private final SurfaceControl.Transaction mStartTransaction;
+    private final int mEnterAnimationDuration;
+    private final @BOUNDS int mDirection;
+
+    // optional callbacks for tracking animation start and end
+    @Nullable private Runnable mAnimationStartCallback;
+    @Nullable private Runnable mAnimationEndCallback;
+
+    private final Rect mBaseBounds = new Rect();
+    private final Rect mStartBounds = new Rect();
+    private final Rect mEndBounds = new Rect();
+
+    // Bounds updated by the evaluator as animator is running.
+    private final Rect mAnimatedRect = new Rect();
+
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final RectEvaluator mRectEvaluator;
+    private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
+
+    public PipEnterExitAnimator(Context context,
+            @NonNull SurfaceControl leash,
+            SurfaceControl.Transaction startTransaction,
+            @NonNull Rect baseBounds,
+            @NonNull Rect startBounds,
+            @NonNull Rect endBounds,
+            @BOUNDS int direction) {
+        mLeash = leash;
+        mStartTransaction = startTransaction;
+        mBaseBounds.set(baseBounds);
+        mStartBounds.set(startBounds);
+        mAnimatedRect.set(startBounds);
+        mEndBounds.set(endBounds);
+        mRectEvaluator = new RectEvaluator(mAnimatedRect);
+        mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context);
+        mDirection = direction;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+        mEnterAnimationDuration = context.getResources()
+                .getInteger(R.integer.config_pipEnterAnimationDuration);
+
+        setDuration(mEnterAnimationDuration);
+        setEvaluator(mRectEvaluator);
+        addListener(this);
+        addUpdateListener(this);
+    }
+
+    public void setAnimationStartCallback(@NonNull Runnable runnable) {
+        mAnimationStartCallback = runnable;
+    }
+
+    public void setAnimationEndCallback(@NonNull Runnable runnable) {
+        mAnimationEndCallback = runnable;
+    }
+
+    @Override
+    public void onAnimationStart(@NonNull Animator animation) {
+        if (mAnimationStartCallback != null) {
+            mAnimationStartCallback.run();
+        }
+        if (mStartTransaction != null) {
+            mStartTransaction.apply();
+        }
+    }
+
+    @Override
+    public void onAnimationEnd(@NonNull Animator animation) {
+        if (mAnimationEndCallback != null) {
+            mAnimationEndCallback.run();
+        }
+    }
+
+    @Override
+    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+        final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+        final float fraction = getAnimatedFraction();
+        // TODO (b/350801661): implement fixed rotation
+
+        mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, null,
+                mBaseBounds, mAnimatedRect, null, isInPipDirection(), fraction)
+                .round(tx, mLeash, isInPipDirection())
+                .shadow(tx, mLeash, isInPipDirection());
+        tx.apply();
+    }
+
+    private boolean isInPipDirection() {
+        return mDirection == BOUNDS_ENTER;
+    }
+
+    // no-ops
+
+    @Override
+    public void onAnimationCancel(@NonNull Animator animation) {}
+
+    @Override
+    public void onAnimationRepeat(@NonNull Animator animation) {}
+}
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 683d30d..33703ad 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
@@ -53,6 +53,7 @@
 import com.android.wm.shell.pip.PipContentOverlay;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
+import com.android.wm.shell.pip2.animation.PipEnterExitAnimator;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
@@ -378,12 +379,34 @@
         if (pipChange == null) {
             return false;
         }
-        // cache the PiP task token and leash
-        WindowContainerToken pipTaskToken = pipChange.getContainer();
 
-        startTransaction.apply();
-        // TODO: b/275910498 Use a new implementation of the PiP animator here.
-        finishCallback.onTransitionFinished(null);
+        WindowContainerToken pipTaskToken = pipChange.getContainer();
+        if (pipTaskToken == null) {
+            return false;
+        }
+
+        WindowContainerTransaction finishWct = new WindowContainerTransaction();
+        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+
+        Rect startBounds = pipChange.getStartAbsBounds();
+        Rect endBounds = pipChange.getEndAbsBounds();
+        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+
+        PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
+                startTransaction, startBounds, startBounds, endBounds,
+                PipEnterExitAnimator.BOUNDS_ENTER);
+
+        tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
+                this::onClientDrawAtTransitionEnd);
+        finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
+
+        animator.setAnimationEndCallback(() -> {
+            mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+            finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
+        });
+
+        animator.start();
         return true;
     }
 
@@ -421,10 +444,25 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
-        startTransaction.apply();
-        // TODO: b/275910498 Use a new implementation of the PiP animator here.
-        finishCallback.onTransitionFinished(null);
-        mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+        TransitionInfo.Change pipChange = getPipChange(info);
+        if (pipChange == null) {
+            return false;
+        }
+
+        Rect startBounds = pipChange.getStartAbsBounds();
+        Rect endBounds = pipChange.getEndAbsBounds();
+        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+
+        PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash,
+                startTransaction, startBounds, startBounds, endBounds,
+                PipEnterExitAnimator.BOUNDS_EXIT);
+        animator.setAnimationEndCallback(() -> {
+            finishCallback.onTransitionFinished(null);
+            mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
+        });
+
+        animator.start();
         return true;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 9539a45..d001b2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -466,7 +466,7 @@
             @Nullable WindowContainerToken ignoreTaskToken) {
         List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2,
                 false /* filterOnlyVisibleRecents */);
-        for (int i = tasks.size() - 1; i >= 0; i--) {
+        for (int i = 0; i < tasks.size(); i++) {
             final ActivityManager.RunningTaskInfo task = tasks.get(i);
             if (task.token.equals(ignoreTaskToken)) {
                 continue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 8df287d..06c57bd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -113,6 +113,9 @@
     /** Called when device waking up finished. */
     void onFinishedWakingUp();
 
+    /** Called when device starts going to sleep (screen off). */
+    void onStartedGoingToSleep();
+
     /** Called when requested to go to fullscreen from the current active split app. */
     void goToFullscreenFromSplit();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index e659151..b857556 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -50,6 +50,7 @@
 import android.content.pm.ShortcutInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArrayMap;
@@ -180,6 +181,7 @@
     private final LauncherApps mLauncherApps;
     private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
     private final ShellExecutor mMainExecutor;
+    private final Handler mMainHandler;
     private final SplitScreenImpl mImpl = new SplitScreenImpl();
     private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
@@ -227,7 +229,8 @@
             Optional<DesktopTasksController> desktopTasksController,
             @Nullable StageCoordinator stageCoordinator,
             MultiInstanceHelper multiInstanceHelper,
-            ShellExecutor mainExecutor) {
+            ShellExecutor mainExecutor,
+            Handler mainHandler) {
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mTaskOrganizer = shellTaskOrganizer;
@@ -236,6 +239,7 @@
         mLauncherApps = context.getSystemService(LauncherApps.class);
         mRootTDAOrganizer = rootTDAOrganizer;
         mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -292,7 +296,7 @@
         return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
-                mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController,
+                mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController,
                 mWindowDecorViewModel);
     }
 
@@ -448,13 +452,17 @@
     @Override
     public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
             boolean animatingDismiss) {
-        mStageCoordinator.onKeyguardVisibilityChanged(visible);
+        mStageCoordinator.onKeyguardStateChanged(visible, occluded);
     }
 
     public void onFinishedWakingUp() {
         mStageCoordinator.onFinishedWakingUp();
     }
 
+    public void onStartedGoingToSleep() {
+        mStageCoordinator.onStartedGoingToSleep();
+    }
+
     public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
         mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
     }
@@ -1201,6 +1209,11 @@
         }
 
         @Override
+        public void onStartedGoingToSleep() {
+            mMainExecutor.execute(SplitScreenController.this::onStartedGoingToSleep);
+        }
+
+        @Override
         public void goToFullscreenFromSplit() {
             mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit);
         }
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 9bcd9b0..a4f32c4 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
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.splitscreen;
 
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
@@ -92,6 +92,7 @@
 import android.hardware.devicestate.DeviceStateManager;
 import android.os.Bundle;
 import android.os.Debug;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -119,6 +120,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
+import com.android.internal.policy.FoldLockSettingsObserver;
 import com.android.internal.protolog.ProtoLog;
 import com.android.internal.util.ArrayUtils;
 import com.android.launcher3.icons.IconProvider;
@@ -191,7 +193,7 @@
     private SplitLayout mSplitLayout;
     private ValueAnimator mDividerFadeInAnimator;
     private boolean mDividerVisible;
-    private boolean mKeyguardShowing;
+    private boolean mKeyguardActive;
     private boolean mShowDecorImmediately;
     private final SyncTransactionQueue mSyncQueue;
     private final ShellTaskOrganizer mTaskOrganizer;
@@ -205,6 +207,7 @@
     private SplitScreenTransitions mSplitTransitions;
     private final SplitscreenEventLogger mLogger;
     private final ShellExecutor mMainExecutor;
+    private final Handler mMainHandler;
     // Cache live tile tasks while entering recents, evict them from stages in finish transaction
     // if user is opening another task(s).
     private final ArrayList<Integer> mPausingTasks = new ArrayList<>();
@@ -233,7 +236,10 @@
     private boolean mIsExiting;
     private boolean mIsRootTranslucent;
     @VisibleForTesting
-    int mTopStageAfterFoldDismiss;
+    @StageType int mLastActiveStage;
+    private boolean mBreakOnNextWake;
+    /** Used to get the Settings value for "Continue using apps on fold". */
+    private FoldLockSettingsObserver mFoldLockSettingsObserver;
 
     private DefaultMixedHandler mMixedHandler;
     private final Toast mSplitUnsupportedToast;
@@ -313,9 +319,8 @@
             ShellTaskOrganizer taskOrganizer, DisplayController displayController,
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
-            TransactionPool transactionPool,
-            IconProvider iconProvider, ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks,
+            TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor,
+            Handler mainHandler, Optional<RecentTasksController> recentTasks,
             LaunchAdjacentController launchAdjacentController,
             Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
@@ -324,6 +329,7 @@
         mTaskOrganizer = taskOrganizer;
         mLogger = new SplitscreenEventLogger();
         mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
         mRecentTasks = recentTasks;
         mLaunchAdjacentController = launchAdjacentController;
         mWindowDecorViewModel = windowDecorViewModel;
@@ -366,6 +372,9 @@
         // With shell transition, we should update recents tile each callback so set this to true by
         // default.
         mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS;
+        mFoldLockSettingsObserver =
+                new FoldLockSettingsObserver(mainHandler, context);
+        mFoldLockSettingsObserver.register();
     }
 
     @VisibleForTesting
@@ -373,9 +382,8 @@
             ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage,
             DisplayController displayController, DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
-            Transitions transitions, TransactionPool transactionPool,
-            ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks,
+            Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor,
+            Handler mainHandler, Optional<RecentTasksController> recentTasks,
             LaunchAdjacentController launchAdjacentController,
             Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
@@ -393,6 +401,7 @@
                 this::onTransitionAnimationComplete, this);
         mLogger = new SplitscreenEventLogger();
         mMainExecutor = mainExecutor;
+        mMainHandler = mainHandler;
         mRecentTasks = recentTasks;
         mLaunchAdjacentController = launchAdjacentController;
         mWindowDecorViewModel = windowDecorViewModel;
@@ -400,6 +409,9 @@
         transitions.addHandler(this);
         mSplitUnsupportedToast = Toast.makeText(mContext,
                 R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
+        mFoldLockSettingsObserver =
+                new FoldLockSettingsObserver(context.getMainThreadHandler(), context);
+        mFoldLockSettingsObserver.register();
     }
 
     public void setMixedHandler(DefaultMixedHandler mixedHandler) {
@@ -1504,51 +1516,80 @@
         }
     }
 
-    void onKeyguardVisibilityChanged(boolean showing) {
-        mKeyguardShowing = showing;
+    /**
+     * Runs when keyguard state changes. The booleans here are a bit complicated, so for reference:
+     * @param active {@code true} if we are in a state where the keyguard *should* be shown
+     *                           -- still true when keyguard is "there" but is behind an app, or
+     *                           screen is off.
+     * @param occludingTaskRunning {@code true} when there is a running task that has
+     *                                         FLAG_SHOW_WHEN_LOCKED -- also true when the task is
+     *                                         just running on its own and keyguard is not active
+     *                                         at all.
+     */
+    void onKeyguardStateChanged(boolean active, boolean occludingTaskRunning) {
+        mKeyguardActive = active;
         if (!mMainStage.isActive()) {
             return;
         }
-        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing);
-        setDividerVisibility(!mKeyguardShowing, null);
+        ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+                "onKeyguardVisibilityChanged: active=%b occludingTaskRunning=%b",
+                active, occludingTaskRunning);
+        setDividerVisibility(!mKeyguardActive, null);
+
+        if (active && occludingTaskRunning) {
+            dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
+        }
     }
 
     void onFinishedWakingUp() {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp");
-        if (!mMainStage.isActive()) {
+        if (mBreakOnNextWake) {
+            dismissSplitKeepingLastActiveStage(EXIT_REASON_DEVICE_FOLDED);
+        }
+    }
+
+    void onStartedGoingToSleep() {
+        recordLastActiveStage();
+    }
+
+    /**
+     * Records the user's last focused stage -- main stage or side stage. Used to determine which
+     * stage of a split pair should be kept, in cases where system focus has moved elsewhere.
+     */
+    void recordLastActiveStage() {
+        if (!isSplitActive() || !isSplitScreenVisible()) {
+            mLastActiveStage = STAGE_TYPE_UNDEFINED;
+        } else if (mMainStage.isFocused()) {
+            mLastActiveStage = STAGE_TYPE_MAIN;
+        } else if (mSideStage.isFocused()) {
+            mLastActiveStage = STAGE_TYPE_SIDE;
+        }
+    }
+
+    /**
+     * Dismisses split, keeping the app that the user focused last in split screen. If the user was
+     * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we
+     * will do a no-op.
+     */
+    void dismissSplitKeepingLastActiveStage(@ExitReason int reason) {
+        if (!mMainStage.isActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) {
+            // no-op
             return;
         }
 
-        // Check if there's only one stage visible while keyguard occluded.
-        final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible;
-        final boolean oneStageVisible =
-                mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible;
-        if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) {
-            // Dismiss split because there's show-when-locked activity showing on top of keyguard.
-            // Also make sure the task contains show-when-locked activity remains on top after split
-            // dismissed.
-            final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage;
-            exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
+        if (ENABLE_SHELL_TRANSITIONS) {
+            // Need manually clear here due to this transition might be aborted due to keyguard
+            // on top and lead to no visible change.
+            clearSplitPairedInRecents(reason);
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            prepareExitSplitScreen(mLastActiveStage, wct);
+            mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason);
+            setSplitsVisible(false);
+        } else {
+            exitSplitScreen(mLastActiveStage == STAGE_TYPE_MAIN ? mMainStage : mSideStage, reason);
         }
 
-        // Dismiss split if the flag record any side of stages.
-        if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
-            if (ENABLE_SHELL_TRANSITIONS) {
-                // Need manually clear here due to this transition might be aborted due to keyguard
-                // on top and lead to no visible change.
-                clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED);
-                final WindowContainerTransaction wct = new WindowContainerTransaction();
-                prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct);
-                mSplitTransitions.startDismissTransition(wct, this,
-                        mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED);
-                setSplitsVisible(false);
-            } else {
-                exitSplitScreen(
-                        mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
-                        EXIT_REASON_DEVICE_FOLDED);
-            }
-            mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
-        }
+        mBreakOnNextWake = false;
     }
 
     void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -1909,8 +1950,8 @@
         }
         // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
         // will be canceled.
-        options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+        options.setPendingIntentBackgroundActivityStartMode(
+                MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
 
         // TODO (b/336477473): Disallow enter PiP when launching a task in split by default;
         //                     this might have to be changed as more split-to-pip cujs are defined.
@@ -2223,11 +2264,11 @@
 
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
                 "setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s",
-                visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller());
+                visible, mKeyguardActive, mIsDividerRemoteAnimating, Debug.getCaller());
 
         // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard
         // dismissing animation.
-        if (visible && mKeyguardShowing) {
+        if (visible && mKeyguardActive) {
             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
                     "   Defer showing divider bar due to keyguard showing.");
             return;
@@ -2597,21 +2638,24 @@
     @VisibleForTesting
     void onFoldedStateChanged(boolean folded) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded);
-        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
-        if (!folded) return;
 
-        if (!isSplitActive() || !isSplitScreenVisible()) return;
-
-        // To avoid split dismiss when user fold the device and unfold to use later, we only
-        // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss
-        // when user interact on phone folded.
-        if (mMainStage.isFocused()) {
-            mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
-        } else if (mSideStage.isFocused()) {
-            mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
+        if (folded) {
+            recordLastActiveStage();
+            // If user folds and has the setting "Continue using apps on fold = NEVER", we assume
+            // they don't want to continue using split on the outer screen (i.e. we break split if
+            // they wake the device in its folded state).
+            mBreakOnNextWake = willSleepOnFold();
+        } else {
+            mBreakOnNextWake = false;
         }
     }
 
+    /** Returns true if the phone will sleep when it folds. */
+    @VisibleForTesting
+    boolean willSleepOnFold() {
+        return mFoldLockSettingsObserver != null && mFoldLockSettingsObserver.isSleepOnFold();
+    }
+
     private Rect getSideStageBounds() {
         return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
                 ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
@@ -3739,8 +3783,9 @@
                 mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
                 Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow",
-                        "app package " + taskInfo.baseActivity.getPackageName()
-                        + " does not support splitscreen, or is a controlled activity type"));
+                        "app package " + taskInfo.baseIntent.getComponent()
+                                + " does not support splitscreen, or is a controlled activity"
+                                + " type"));
                 if (splitScreenVisible) {
                     handleUnsupportedSplitStart();
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
index e330f3a..b65e978 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
@@ -33,7 +33,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.StageCoordinator;
@@ -88,7 +87,8 @@
                 syncQueue, rootTDAOrganizer, displayController, displayImeController,
                 displayInsetsController, null, transitions, transactionPool,
                 iconProvider, recentTasks, launchAdjacentController, Optional.empty(),
-                Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor);
+                Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor,
+                mainHandler);
 
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
index 7947691..81ca48f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
@@ -56,7 +56,7 @@
             SystemWindows systemWindows) {
         super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController,
                 displayInsetsController, transitions, transactionPool, iconProvider,
-                mainExecutor, recentTasks, launchAdjacentController, Optional.empty());
+                mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty());
 
         mTvSplitMenuController = new TvSplitMenuController(context, this,
                 systemWindows, mainHandler);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
index 5c814dc..bad5baf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
@@ -73,7 +73,7 @@
         final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId);
         final StartingSurfaceDrawer.WindowlessStartingWindow wlw =
                 new StartingSurfaceDrawer.WindowlessStartingWindow(
-                runningTaskInfo.configuration, rootSurface);
+                        mContext.getResources().getConfiguration(), rootSurface);
         final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost(
                 mContext, display, wlw, "WindowlessSnapshotWindowCreator");
         final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java
index 98a8031..f372557 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java
@@ -76,7 +76,7 @@
         }
         final StartingSurfaceDrawer.WindowlessStartingWindow wlw =
                 new StartingSurfaceDrawer.WindowlessStartingWindow(
-                        taskInfo.configuration, rootSurface);
+                        mContext.getResources().getConfiguration(), rootSurface);
         final SurfaceControlViewHost viewHost = new SurfaceControlViewHost(
                 myContext, display, wlw, "WindowlessSplashWindowCreator");
         final String title = "Windowless Splash " + taskInfo.taskId;
@@ -95,7 +95,7 @@
         }
 
         final FrameLayout rootLayout = new FrameLayout(
-                mSplashscreenContentDrawer.createViewContextWrapper(mContext));
+                mSplashscreenContentDrawer.createViewContextWrapper(myContext));
         viewHost.setView(rootLayout, lp);
 
         final int bgColor = taskDescription.getBackgroundColor();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index dd4595a..287e779 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -48,6 +48,7 @@
 
     public ShellInit(ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
+        ProtoLog.registerGroups(ShellProtoLogGroup.values());
     }
 
     /**
@@ -76,7 +77,6 @@
      */
     @VisibleForTesting
     public void init() {
-        ProtoLog.registerGroups(ShellProtoLogGroup.values());
         ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size());
         SurfaceControl.setDebugUsageAfterRelease(true);
         // Init in order of registration
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 b9cb6d3..5c230c0 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
@@ -56,6 +56,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
@@ -72,6 +73,7 @@
     private final IWindowManager mWindowManager;
     private final Context mContext;
     private final Handler mMainHandler;
+    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final ShellExecutor mMainExecutor;
     private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
@@ -108,6 +110,7 @@
     public CaptionWindowDecorViewModel(
             Context context,
             Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellExecutor shellExecutor,
             Choreographer mainChoreographer,
             IWindowManager windowManager,
@@ -120,6 +123,7 @@
         mContext = context;
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
+        mBgExecutor = bgExecutor;
         mWindowManager = windowManager;
         mMainChoreographer = mainChoreographer;
         mTaskOrganizer = taskOrganizer;
@@ -289,6 +293,7 @@
                         taskInfo,
                         taskSurface,
                         mMainHandler,
+                        mBgExecutor,
                         mMainChoreographer,
                         mSyncQueue);
         mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
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 7e1b973..cf42a49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -21,6 +21,7 @@
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
@@ -48,7 +49,9 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
 
 /**
@@ -58,6 +61,7 @@
  */
 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
     private final Handler mHandler;
+    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
 
@@ -78,10 +82,12 @@
             RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             Handler handler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue) {
         super(context, displayController, taskOrganizer, taskInfo, taskSurface);
         mHandler = handler;
+        mBgExecutor = bgExecutor;
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
     }
@@ -218,6 +224,7 @@
         relayoutParams.mOccludingCaptionElements.add(controlsElement);
     }
 
+    @SuppressLint("MissingPermission")
     void relayout(RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
@@ -235,7 +242,7 @@
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
 
-        mTaskOrganizer.applyTransaction(wct);
+        mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
 
         if (mResult.mRootView == null) {
             // This means something blocks the window decor from showing, e.g. the task is hidden.
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 5397625..8312aef 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
@@ -22,6 +22,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.content.Intent.ACTION_MAIN;
+import static android.content.Intent.CATEGORY_APP_BROWSER;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
 import static android.view.MotionEvent.ACTION_CANCEL;
@@ -36,6 +38,8 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 
@@ -43,11 +47,8 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -98,6 +99,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
 import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
@@ -132,6 +134,7 @@
     private final ShellController mShellController;
     private final Context mContext;
     private final Handler mMainHandler;
+    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final Choreographer mMainChoreographer;
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
@@ -183,6 +186,7 @@
             ShellExecutor shellExecutor,
             Handler mainHandler,
             Choreographer mainChoreographer,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
             IWindowManager windowManager,
@@ -201,6 +205,7 @@
                 shellExecutor,
                 mainHandler,
                 mainChoreographer,
+                bgExecutor,
                 shellInit,
                 shellCommandHandler,
                 windowManager,
@@ -225,6 +230,7 @@
             ShellExecutor shellExecutor,
             Handler mainHandler,
             Choreographer mainChoreographer,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             ShellInit shellInit,
             ShellCommandHandler shellCommandHandler,
             IWindowManager windowManager,
@@ -245,6 +251,7 @@
         mMainExecutor = shellExecutor;
         mMainHandler = mainHandler;
         mMainChoreographer = mainChoreographer;
+        mBgExecutor = bgExecutor;
         mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mTaskOrganizer = taskOrganizer;
         mShellController = shellController;
@@ -321,7 +328,8 @@
         if (decoration == null) return;
         final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo;
 
-        if (taskInfo.displayId != oldTaskInfo.displayId) {
+        if (taskInfo.displayId != oldTaskInfo.displayId
+                && !Flags.enableAdditionalWindowsAboveStatusBar()) {
             removeTaskFromEventReceiver(oldTaskInfo.displayId);
             incrementEventReceiverTasks(taskInfo.displayId);
         }
@@ -385,7 +393,8 @@
 
         decoration.close();
         final int displayId = taskInfo.displayId;
-        if (mEventReceiversByDisplay.contains(displayId)) {
+        if (mEventReceiversByDisplay.contains(displayId)
+                && !Flags.enableAdditionalWindowsAboveStatusBar()) {
             removeTaskFromEventReceiver(displayId);
         }
         // Remove the decoration from the cache last because WindowDecoration#close could still
@@ -424,19 +433,12 @@
     }
 
     private void openInBrowser(Uri uri) {
-        final Intent intent = new Intent(Intent.ACTION_VIEW, uri)
-                .setComponent(getDefaultBrowser())
+        final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER)
+                .setData(uri)
                 .addFlags(FLAG_ACTIVITY_NEW_TASK);
         mContext.startActivity(intent);
     }
 
-    private ComponentName getDefaultBrowser() {
-        final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://"));
-        final ResolveInfo info = mContext.getPackageManager()
-                .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-        return info.getComponentInfo().getComponentName();
-    }
-
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             View.OnGenericMotionListener, DragDetector.MotionEventHandler {
@@ -517,6 +519,9 @@
                 }
             } else if (id == R.id.split_screen_button) {
                 decoration.closeHandleMenu();
+                // When the app enters split-select, the handle will no longer be visible, meaning
+                // we shouldn't receive input for it any longer.
+                decoration.disposeStatusBarInputLayer();
                 mDesktopTasksController.requestSplit(decoration.mTaskInfo);
             } else if (id == R.id.open_in_browser_button) {
                 // TODO(b/346441962): let the decoration handle the click gesture and only call back
@@ -653,13 +658,38 @@
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final RunningTaskInfo taskInfo = decoration.mTaskInfo;
             if (DesktopModeStatus.canEnterDesktopMode(mContext)
-                    && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
-                return false;
+                    && !taskInfo.isFreeform()) {
+                return handleNonFreeformMotionEvent(decoration, v, e);
+            } else {
+                return handleFreeformMotionEvent(decoration, taskInfo, v, e);
             }
+        }
+
+        private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration,
+                View v, MotionEvent e) {
+            final int id = v.getId();
+            if (id == R.id.caption_handle) {
+                if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                    // Caption handle is located within the status bar region, meaning the
+                    // DisplayPolicy will attempt to transfer this input to status bar if it's
+                    // a swipe down. Pilfer here to keep the gesture in handle alone.
+                    mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
+                }
+                handleCaptionThroughStatusBar(e, decoration);
+                final boolean wasDragging = mIsDragging;
+                updateDragStatus(e.getActionMasked());
+                // Only prevent onClick from receiving this event if it's a drag.
+                return wasDragging;
+            }
+            return false;
+        }
+
+        private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration,
+                RunningTaskInfo taskInfo, View v, MotionEvent e) {
+            final int id = v.getId();
             if (mGestureDetector.onTouchEvent(e)) {
                 return true;
             }
-            final int id = v.getId();
             final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window
                     || id == R.id.open_menu_button);
             switch (e.getActionMasked()) {
@@ -668,7 +698,7 @@
                     mDragPositioningCallback.onDragPositioningStart(
                             0 /* ctrlType */, e.getRawX(0),
                             e.getRawY(0));
-                    mIsDragging = false;
+                    updateDragStatus(e.getActionMasked());
                     mHasLongClicked = false;
                     // Do not consume input event if a button is touched, otherwise it would
                     // prevent the button's ripple effect from showing.
@@ -688,7 +718,7 @@
                             decoration.mTaskSurface,
                             e.getRawX(dragPointerIdx),
                             newTaskBounds);
-                    mIsDragging = true;
+                    updateDragStatus(e.getActionMasked());
                     return true;
                 }
                 case MotionEvent.ACTION_UP:
@@ -718,7 +748,7 @@
                         // onClick call that results.
                         return false;
                     } else {
-                        mIsDragging = false;
+                        updateDragStatus(e.getActionMasked());
                         return true;
                     }
                 }
@@ -726,6 +756,21 @@
             return true;
         }
 
+        private void updateDragStatus(int eventAction) {
+            switch (eventAction) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL: {
+                    mIsDragging = false;
+                    break;
+                }
+                case MotionEvent.ACTION_MOVE: {
+                    mIsDragging = true;
+                    break;
+                }
+            }
+        }
+
         /**
          * Perform a task size toggle on release of the double-tap, assuming no drag event
          * was handled during the double-tap.
@@ -850,6 +895,10 @@
      *
      * @param relevantDecor the window decoration of the focused task's caption. This method only
      *                      handles motion events outside this caption's bounds.
+     * TODO(b/349135068): Outside-touch detection no longer works with the
+     *  enableAdditionalWindowsAboveStatusBar flag enabled. This
+     *  will be fixed once we can add FLAG_WATCH_OUTSIDE_TOUCH to relevant menus,
+     *  at which point, all EventReceivers and external touch logic should be removed.
      */
     private void handleEventOutsideCaption(MotionEvent ev,
             DesktopModeWindowDecoration relevantDecor) {
@@ -902,9 +951,10 @@
                     dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
                             || windowingMode == WINDOWING_MODE_MULTI_WINDOW;
                 }
-
-                if (dragFromStatusBarAllowed
-                        && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
+                final boolean shouldStartTransitionDrag =
+                        relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
+                        || Flags.enableAdditionalWindowsAboveStatusBar();
+                if (dragFromStatusBarAllowed && shouldStartTransitionDrag) {
                     mTransitionDragActive = true;
                 }
                 break;
@@ -918,8 +968,15 @@
                         // Though this isn't a hover event, we need to update handle's hover state
                         // as it likely will change.
                         relevantDecor.updateHoverAndPressStatus(ev);
-                        mDesktopTasksController.onDragPositioningEndThroughStatusBar(
+                        DesktopModeVisualIndicator.IndicatorType resultType =
+                                mDesktopTasksController.onDragPositioningEndThroughStatusBar(
                                 new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo);
+                        // If we are entering split select, handle will no longer be visible and
+                        // should not be receiving any input.
+                        if (resultType == TO_SPLIT_LEFT_INDICATOR
+                                || resultType != TO_SPLIT_RIGHT_INDICATOR) {
+                            relevantDecor.disposeStatusBarInputLayer();
+                        }
                         mMoveToDesktopAnimator = null;
                         return;
                     } else {
@@ -931,7 +988,6 @@
                 relevantDecor.checkTouchEvent(ev);
                 break;
             }
-
             case ACTION_MOVE: {
                 if (relevantDecor == null) {
                     return;
@@ -1091,10 +1147,12 @@
                 mDesktopModeWindowDecorFactory.create(
                         mContext,
                         mDisplayController,
+                        mSplitScreenController,
                         mTaskOrganizer,
                         taskInfo,
                         taskSurface,
                         mMainHandler,
+                        mBgExecutor,
                         mMainChoreographer,
                         mSyncQueue,
                         mRootTaskDisplayAreaOrganizer);
@@ -1132,7 +1190,9 @@
         windowDecoration.setDragDetector(touchEventListener.mDragDetector);
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
-        incrementEventReceiverTasks(taskInfo.displayId);
+        if (!Flags.enableAdditionalWindowsAboveStatusBar()) {
+            incrementEventReceiverTasks(taskInfo.displayId);
+        }
     }
 
     private RunningTaskInfo getOtherSplitTask(int taskId) {
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 5d662b2..529def7 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
@@ -24,11 +24,13 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
 import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.ComponentName;
@@ -68,7 +70,9 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
@@ -95,8 +99,10 @@
     static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L;
 
     private final Handler mHandler;
+    private final @ShellBackgroundThread ShellExecutor mBgExecutor;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
+    private final SplitScreenController mSplitScreenController;
 
     private WindowDecorationViewHolder mWindowDecorViewHolder;
     private View.OnClickListener mOnCaptionButtonClickListener;
@@ -147,27 +153,32 @@
     DesktopModeWindowDecoration(
             Context context,
             DisplayController displayController,
+            SplitScreenController splitScreenController,
             ShellTaskOrganizer taskOrganizer,
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             Handler handler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
-        this (context, displayController, taskOrganizer, taskInfo, taskSurface,
-                handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
-                SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
-                WindowContainerTransaction::new, SurfaceControl::new,
-                new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE);
+        this (context, displayController, splitScreenController, taskOrganizer, taskInfo,
+                taskSurface, handler, bgExecutor, choreographer, syncQueue,
+                rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new,
+                SurfaceControl.Transaction::new,  WindowContainerTransaction::new,
+                SurfaceControl::new, new SurfaceControlViewHostFactory() {},
+                DefaultMaximizeMenuFactory.INSTANCE);
     }
 
     DesktopModeWindowDecoration(
             Context context,
             DisplayController displayController,
+            SplitScreenController splitScreenController,
             ShellTaskOrganizer taskOrganizer,
             ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
             Handler handler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             Choreographer choreographer,
             SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
@@ -181,7 +192,9 @@
                 surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
                 windowContainerTransactionSupplier, surfaceControlSupplier,
                 surfaceControlViewHostFactory);
+        mSplitScreenController = splitScreenController;
         mHandler = handler;
+        mBgExecutor = bgExecutor;
         mChoreographer = choreographer;
         mSyncQueue = syncQueue;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
@@ -327,6 +340,7 @@
         mHandler.post(mCurrentViewHostRunnable);
     }
 
+    @SuppressLint("MissingPermission")
     private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
@@ -337,7 +351,7 @@
         }
 
         if (isHandleMenuActive()) {
-            mHandleMenu.relayout(startT);
+            mHandleMenu.relayout(startT, mResult.mCaptionX);
         }
 
         updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
@@ -353,33 +367,47 @@
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
 
         Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT");
-        mTaskOrganizer.applyTransaction(wct);
+        mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct));
         Trace.endSection();
 
         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.
+            disposeStatusBarInputLayer();
             Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
             return;
         }
 
         if (oldRootView != mResult.mRootView) {
+            disposeStatusBarInputLayer();
             mWindowDecorViewHolder = createViewHolder();
         }
         Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
-        mWindowDecorViewHolder.bindData(mTaskInfo);
+
+        final Point position = new Point();
+        if (isAppHandle(mWindowDecorViewHolder)) {
+            position.set(determineHandlePosition());
+        }
+        mWindowDecorViewHolder.bindData(mTaskInfo,
+                position,
+                mResult.mCaptionWidth,
+                mResult.mCaptionHeight,
+                isCaptionVisible());
         Trace.endSection();
 
         if (!mTaskInfo.isFocused) {
             closeHandleMenu();
             closeMaximizeMenu();
         }
-
         updateDragResizeListener(oldDecorationSurface);
         updateMaximizeMenu(startT);
         Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
     }
 
+    private boolean isCaptionVisible() {
+        return mTaskInfo.isVisible && mIsCaptionVisible;
+    }
+
     private void setCapturedLink(Uri capturedLink, long timeStamp) {
         if (capturedLink == null
                 || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) {
@@ -461,12 +489,42 @@
         }
     }
 
+    private Point determineHandlePosition() {
+        final Point position = new Point(mResult.mCaptionX, 0);
+        if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId)
+                == SPLIT_POSITION_BOTTOM_OR_RIGHT
+                && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape()
+        ) {
+            // If this is the right split task, add left stage's width.
+            final Rect leftStageBounds = new Rect();
+            mSplitScreenController.getStageBounds(leftStageBounds, new Rect());
+            position.x += leftStageBounds.width();
+        }
+        return position;
+    }
+
+    /**
+     * Dispose of the view used to forward inputs in status bar region. Intended to be
+     * used any time handle is no longer visible.
+     */
+    void disposeStatusBarInputLayer() {
+        if (!isAppHandle(mWindowDecorViewHolder)
+                || !Flags.enableAdditionalWindowsAboveStatusBar()) {
+            return;
+        }
+        ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer();
+    }
+
     private WindowDecorationViewHolder createViewHolder() {
         if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) {
             return new AppHandleViewHolder(
                     mResult.mRootView,
                     mOnCaptionTouchListener,
-                    mOnCaptionButtonClickListener
+                    mOnCaptionButtonClickListener,
+                    (v, event) -> {
+                        updateHoverAndPressStatus(event);
+                        return true;
+                    }
             );
         } else if (mRelayoutParams.mLayoutResId
                 == R.layout.desktop_mode_app_header) {
@@ -489,6 +547,10 @@
         throw new IllegalArgumentException("Unexpected layout resource id");
     }
 
+    private boolean isAppHandle(WindowDecorationViewHolder viewHolder) {
+        return viewHolder instanceof AppHandleViewHolder;
+    }
+
     @VisibleForTesting
     static void updateRelayoutParams(
             RelayoutParams relayoutParams,
@@ -863,7 +925,9 @@
                 splitScreenController,
                 DesktopModeStatus.canEnterDesktopMode(mContext),
                 browserLinkAvailable(),
-                mResult.mCaptionHeight
+                mResult.mCaptionWidth,
+                mResult.mCaptionHeight,
+                mResult.mCaptionX
         );
         mWindowDecorViewHolder.onHandleMenuOpened();
         mHandleMenu.show();
@@ -955,10 +1019,17 @@
      * @return {@code true} if event is inside caption handle view, {@code false} if not
      */
     boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
-        if (isHandleMenuActive() || !(mWindowDecorViewHolder
-                instanceof AppHandleViewHolder)) {
+        if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder)
+                || Flags.enableAdditionalWindowsAboveStatusBar()) {
             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.enableAdditionalWindowsAboveStatusBar()
+                && isCaptionVisible()) {
+            return true;
+        }
 
         return checkTouchEventInCaption(ev);
     }
@@ -992,7 +1063,7 @@
      * @param ev the MotionEvent to compare
      */
     void checkTouchEvent(MotionEvent ev) {
-        if (mResult.mRootView == null) return;
+        if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) 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()
@@ -1014,7 +1085,7 @@
      * @param ev the MotionEvent to compare against.
      */
     void updateHoverAndPressStatus(MotionEvent ev) {
-        if (mResult.mRootView == null) return;
+        if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return;
         final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
         final boolean inHandle = !isHandleMenuActive()
                 && checkTouchEventInFocusedCaptionHandle(ev);
@@ -1024,15 +1095,11 @@
         // We want handle to remain pressed if the pointer moves outside of it during a drag.
         handle.setPressed((inHandle && action == ACTION_DOWN)
                 || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL));
-        if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) {
+        if (isHandleMenuActive()) {
             mHandleMenu.checkMotionEvent(ev);
         }
     }
 
-    private boolean isHandleMenuAboveStatusBar() {
-        return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform();
-    }
-
     private boolean pointInView(View v, float x, float y) {
         return v != null && v.getLeft() <= x && v.getRight() >= x
                 && v.getTop() <= y && v.getBottom() >= y;
@@ -1044,6 +1111,7 @@
         closeHandleMenu();
         mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
         disposeResizeVeil();
+        disposeStatusBarInputLayer();
         clearCurrentViewHostRunnable();
         super.close();
     }
@@ -1143,20 +1211,24 @@
         DesktopModeWindowDecoration create(
                 Context context,
                 DisplayController displayController,
+                SplitScreenController splitScreenController,
                 ShellTaskOrganizer taskOrganizer,
                 ActivityManager.RunningTaskInfo taskInfo,
                 SurfaceControl taskSurface,
                 Handler handler,
+                @ShellBackgroundThread ShellExecutor bgExecutor,
                 Choreographer choreographer,
                 SyncTransactionQueue syncQueue,
                 RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
             return new DesktopModeWindowDecoration(
                     context,
                     displayController,
+                    splitScreenController,
                     taskOrganizer,
                     taskInfo,
                     taskSurface,
                     handler,
+                    bgExecutor,
                     choreographer,
                     syncQueue,
                     rootTaskDisplayAreaOrganizer);
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 bce233f..e174e83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -15,19 +15,15 @@
  */
 package com.android.wm.shell.windowdecor
 
+import android.annotation.ColorInt
 import android.annotation.DimenRes
 import android.app.ActivityManager
-import android.app.WindowConfiguration
-import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
-import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
-import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
-import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.content.Context
 import android.content.res.ColorStateList
-import android.content.res.Configuration
 import android.content.res.Resources
 import android.graphics.Bitmap
-import android.graphics.Color
+import android.graphics.BlendMode
+import android.graphics.BlendModeColorFilter
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
@@ -40,6 +36,8 @@
 import android.widget.TextView
 import android.window.SurfaceSyncGroup
 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.common.DisplayController
@@ -47,7 +45,10 @@
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
 import com.android.wm.shell.windowdecor.extension.isFullscreen
+import com.android.wm.shell.windowdecor.extension.isMultiWindow
+import com.android.wm.shell.windowdecor.extension.isPinned
 
 /**
  * Handle menu opened when the appropriate button is clicked on.
@@ -68,10 +69,13 @@
     private val splitScreenController: SplitScreenController,
     private val shouldShowWindowingPill: Boolean,
     private val shouldShowBrowserPill: Boolean,
-    private val captionHeight: Int
+    private val captionWidth: Int,
+    private val captionHeight: Int,
+    captionX: Int
 ) {
     private val context: Context = parentDecor.mDecorWindowContext
     private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo
+    private val decorThemeUtil = DecorThemeUtil(context)
 
     private val isViewAboveStatusBar: Boolean
         get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform)
@@ -102,33 +106,8 @@
     // those as well.
     private val globalMenuPosition: Point = Point()
 
-    /**
-     * An a array of windowing icon color based on current UI theme. First element of the
-     * array is for inactive icons and the second is for active icons.
-     */
-    private val windowingIconColor: Array<ColorStateList>
-        get() {
-            val mode = (context.resources.configuration.uiMode
-                    and Configuration.UI_MODE_NIGHT_MASK)
-            val isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES)
-            val typedArray = context.obtainStyledAttributes(
-                intArrayOf(
-                    com.android.internal.R.attr.materialColorOnSurface,
-                    com.android.internal.R.attr.materialColorPrimary
-                )
-            )
-            val inActiveColor =
-                typedArray.getColor(0, if (isNightMode) Color.WHITE else Color.BLACK)
-            val activeColor = typedArray.getColor(1, if (isNightMode) Color.WHITE else Color.BLACK)
-            typedArray.recycle()
-            return arrayOf(
-                ColorStateList.valueOf(inActiveColor),
-                ColorStateList.valueOf(activeColor)
-            )
-        }
-
     init {
-        updateHandleMenuPillPositions()
+        updateHandleMenuPillPositions(captionX)
     }
 
     fun show() {
@@ -175,9 +154,8 @@
      * Animates the appearance of the handle menu and its three pills.
      */
     private fun animateHandleMenu() {
-        when (taskInfo.windowingMode) {
-            WindowConfiguration.WINDOWING_MODE_FULLSCREEN,
-            WINDOWING_MODE_MULTI_WINDOW -> {
+        when {
+            taskInfo.isFullscreen || taskInfo.isMultiWindow -> {
                 handleMenuAnimator?.animateCaptionHandleExpandToOpen()
             }
             else -> {
@@ -193,95 +171,104 @@
     private fun setupHandleMenu() {
         val handleMenu = handleMenuViewContainer?.view ?: return
         handleMenu.setOnTouchListener(onTouchListener)
-        setupAppInfoPill(handleMenu)
+
+        val style = calculateMenuStyle()
+        setupAppInfoPill(handleMenu, style)
         if (shouldShowWindowingPill) {
-            setupWindowingPill(handleMenu)
+            setupWindowingPill(handleMenu, style)
         }
-        setupMoreActionsPill(handleMenu)
-        setupOpenInBrowserPill(handleMenu)
+        setupMoreActionsPill(handleMenu, style)
+        setupOpenInBrowserPill(handleMenu, style)
     }
 
     /**
      * Set up interactive elements of handle menu's app info pill.
      */
-    private fun setupAppInfoPill(handleMenu: View) {
-        val collapseBtn = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button)
-        val appIcon = handleMenu.findViewById<ImageView>(R.id.application_icon)
-        val appName = handleMenu.findViewById<TextView>(R.id.application_name)
-        collapseBtn.setOnClickListener(onClickListener)
-        collapseBtn.taskInfo = taskInfo
-        appIcon.setImageBitmap(appIconBitmap)
-        appName.text = this.appName
+    private fun setupAppInfoPill(handleMenu: View, style: MenuStyle) {
+        val pill = handleMenu.requireViewById<View>(R.id.app_info_pill).apply {
+            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+        }
+
+        pill.requireViewById<HandleMenuImageButton>(R.id.collapse_menu_button)
+            .let { collapseBtn ->
+                collapseBtn.imageTintList = ColorStateList.valueOf(style.textColor)
+                collapseBtn.setOnClickListener(onClickListener)
+                collapseBtn.taskInfo = taskInfo
+            }
+        pill.requireViewById<ImageView>(R.id.application_icon).let { appIcon ->
+            appIcon.setImageBitmap(appIconBitmap)
+        }
+        pill.requireViewById<TextView>(R.id.application_name).let { appNameView ->
+            appNameView.text = appName
+            appNameView.setTextColor(style.textColor)
+        }
     }
 
     /**
      * Set up interactive elements and color of handle menu's windowing pill.
      */
-    private fun setupWindowingPill(handleMenu: View) {
-        val fullscreenBtn = handleMenu.findViewById<ImageButton>(R.id.fullscreen_button)
-        val splitscreenBtn = handleMenu.findViewById<ImageButton>(R.id.split_screen_button)
-        val floatingBtn = handleMenu.findViewById<ImageButton>(R.id.floating_button)
+    private fun setupWindowingPill(handleMenu: View, style: MenuStyle) {
+        val pill = handleMenu.requireViewById<View>(R.id.windowing_pill).apply {
+            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+        }
+
+        val fullscreenBtn = pill.requireViewById<ImageButton>(R.id.fullscreen_button)
+        val splitscreenBtn = pill.requireViewById<ImageButton>(R.id.split_screen_button)
+        val floatingBtn = pill.requireViewById<ImageButton>(R.id.floating_button)
         // TODO: Remove once implemented.
         floatingBtn.visibility = View.GONE
+        val desktopBtn = handleMenu.requireViewById<ImageButton>(R.id.desktop_button)
 
-        val desktopBtn = handleMenu.findViewById<ImageButton>(R.id.desktop_button)
         fullscreenBtn.setOnClickListener(onClickListener)
         splitscreenBtn.setOnClickListener(onClickListener)
         floatingBtn.setOnClickListener(onClickListener)
         desktopBtn.setOnClickListener(onClickListener)
-        // The button corresponding to the windowing mode that the task is currently in uses a
-        // different color than the others.
-        val iconColors = windowingIconColor
-        val inActiveColorStateList = iconColors[0]
-        val activeColorStateList = iconColors[1]
-        fullscreenBtn.imageTintList = if (taskInfo.isFullscreen) {
-            activeColorStateList
-        } else {
-            inActiveColorStateList
-        }
-        splitscreenBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
-            activeColorStateList
-        } else {
-            inActiveColorStateList
-        }
-        floatingBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_PINNED) {
-            activeColorStateList
-        } else {
-            inActiveColorStateList
-        }
-        desktopBtn.imageTintList = if (taskInfo.isFreeform) {
-            activeColorStateList
-        } else {
-            inActiveColorStateList
-        }
+
+        fullscreenBtn.isSelected = taskInfo.isFullscreen
+        fullscreenBtn.imageTintList = style.windowingButtonColor
+        splitscreenBtn.isSelected = taskInfo.isMultiWindow
+        splitscreenBtn.imageTintList = style.windowingButtonColor
+        floatingBtn.isSelected = taskInfo.isPinned
+        floatingBtn.imageTintList = style.windowingButtonColor
+        desktopBtn.isSelected = taskInfo.isFreeform
+        desktopBtn.imageTintList = style.windowingButtonColor
     }
 
     /**
      * Set up interactive elements & height of handle menu's more actions pill
      */
-    private fun setupMoreActionsPill(handleMenu: View) {
-        if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
-            handleMenu.findViewById<View>(R.id.more_actions_pill).visibility = View.GONE
+    private fun setupMoreActionsPill(handleMenu: View, style: MenuStyle) {
+        val pill = handleMenu.requireViewById<View>(R.id.more_actions_pill).apply {
+            isGone = !SHOULD_SHOW_MORE_ACTIONS_PILL
+            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+        }
+        pill.requireViewById<Button>(R.id.screenshot_button).let { screenshotBtn ->
+            screenshotBtn.setTextColor(style.textColor)
+            screenshotBtn.compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
         }
     }
 
-    private fun setupOpenInBrowserPill(handleMenu: View) {
-        if (!shouldShowBrowserPill) {
-            handleMenu.findViewById<View>(R.id.open_in_browser_pill).visibility = View.GONE
-            return
+    private fun setupOpenInBrowserPill(handleMenu: View, style: MenuStyle) {
+        val pill = handleMenu.requireViewById<View>(R.id.open_in_browser_pill).apply {
+            isGone = !shouldShowBrowserPill
+            background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
         }
-        val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button)
-        browserButton.setOnClickListener(onClickListener)
+
+        pill.requireViewById<Button>(R.id.open_in_browser_button).let { browserButton ->
+            browserButton.setOnClickListener(onClickListener)
+            browserButton.setTextColor(style.textColor)
+            browserButton.compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+        }
     }
 
     /**
      * Updates handle menu's position variables to reflect its next position.
      */
-    private fun updateHandleMenuPillPositions() {
+    private fun updateHandleMenuPillPositions(captionX: Int) {
         val menuX: Int
         val menuY: Int
         val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds
-        updateGlobalMenuPosition(taskBounds)
+        updateGlobalMenuPosition(taskBounds, captionX)
         if (layoutResId == R.layout.desktop_mode_app_header) {
             // Align the handle menu to the left side of the caption.
             menuX = marginMenuStart
@@ -302,21 +289,22 @@
         handleMenuPosition.set(menuX.toFloat(), menuY.toFloat())
     }
 
-    private fun updateGlobalMenuPosition(taskBounds: Rect) {
-        when (taskInfo.windowingMode) {
-            WINDOWING_MODE_FREEFORM -> {
+    private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) {
+        val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2)
+        when {
+            taskInfo.isFreeform -> {
                 globalMenuPosition.set(
                     /* x = */ taskBounds.left + marginMenuStart,
                     /* y = */ taskBounds.top + marginMenuTop
                 )
             }
-            WINDOWING_MODE_FULLSCREEN -> {
+            taskInfo.isFullscreen -> {
                 globalMenuPosition.set(
-                    /* x = */ taskBounds.width() / 2 - (menuWidth / 2),
+                    /* x = */ nonFreeformX,
                     /* y = */ marginMenuTop
                 )
             }
-            WINDOWING_MODE_MULTI_WINDOW -> {
+            taskInfo.isMultiWindow -> {
                 val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId)
                 val leftOrTopStageBounds = Rect()
                 val rightOrBottomStageBounds = Rect()
@@ -326,16 +314,13 @@
                 when (splitPosition) {
                     SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> {
                         globalMenuPosition.set(
-                            /* x = */ leftOrTopStageBounds.width()
-                                    + (rightOrBottomStageBounds.width() / 2)
-                                    - (menuWidth / 2),
+                            /* x = */ leftOrTopStageBounds.width() + nonFreeformX,
                             /* y = */ marginMenuTop
                         )
                     }
                     SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> {
                         globalMenuPosition.set(
-                            /* x = */ (leftOrTopStageBounds.width() / 2)
-                                    - (menuWidth / 2),
+                            /* x = */ nonFreeformX,
                             /* y = */ marginMenuTop
                         )
                     }
@@ -347,9 +332,12 @@
     /**
      * Update pill layout, in case task changes have caused positioning to change.
      */
-    fun relayout(t: SurfaceControl.Transaction) {
+    fun relayout(
+        t: SurfaceControl.Transaction,
+        captionX: Int
+    ) {
         handleMenuViewContainer?.let { container ->
-            updateHandleMenuPillPositions()
+            updateHandleMenuPillPositions(captionX)
             container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y)
         }
     }
@@ -469,14 +457,41 @@
             handleMenuViewContainer?.releaseView()
             handleMenuViewContainer = null
         }
-        if (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
-            taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+        if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
             handleMenuAnimator?.animateCollapseIntoHandleClose(after)
         } else {
             handleMenuAnimator?.animateClose(after)
         }
     }
 
+    private fun calculateMenuStyle(): MenuStyle {
+        val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
+        return MenuStyle(
+            backgroundColor = colorScheme.surfaceBright.toArgb(),
+            textColor = colorScheme.onSurface.toArgb(),
+            windowingButtonColor = ColorStateList(
+                arrayOf(
+                    intArrayOf(android.R.attr.state_pressed),
+                    intArrayOf(android.R.attr.state_focused),
+                    intArrayOf(android.R.attr.state_selected),
+                    intArrayOf(),
+                ),
+                intArrayOf(
+                    colorScheme.onSurface.toArgb(),
+                    colorScheme.onSurface.toArgb(),
+                    colorScheme.primary.toArgb(),
+                    colorScheme.onSurface.toArgb(),
+                )
+            ),
+        )
+    }
+
+    private data class MenuStyle(
+        @ColorInt val backgroundColor: Int,
+        @ColorInt val textColor: Int,
+        val windowingButtonColor: ColorStateList,
+    )
+
     companion object {
         private const val TAG = "HandleMenu"
         private const val SHOULD_SHOW_MORE_ACTIONS_PILL = 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 d212f21..a691f59 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
@@ -139,7 +139,7 @@
     private SurfaceControlViewHost mViewHost;
     private Configuration mWindowDecorConfig;
     TaskDragResizer mTaskDragResizer;
-    private boolean mIsCaptionVisible;
+    boolean mIsCaptionVisible;
 
     /** The most recent set of insets applied to this window decoration. */
     private WindowDecorationInsets mWindowDecorationInsets;
@@ -508,6 +508,8 @@
         mTaskDragResizer = taskDragResizer;
     }
 
+    // TODO(b/346441962): Move these three methods closer to implementing or View-level classes to
+    //  keep implementation details more encapsulated.
     private void setCaptionVisibility(View rootView, boolean visible) {
         if (rootView == null) {
             return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 4897f76..6a354f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -30,17 +30,22 @@
  */
 class AdditionalSystemViewContainer(
     private val context: Context,
-    layoutId: Int,
     taskId: Int,
     x: Int,
     y: Int,
     width: Int,
-    height: Int
+    height: Int,
+    layoutId: Int? = null
 ) : AdditionalViewContainer() {
     override val view: View
+    val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java)
 
     init {
-        view = LayoutInflater.from(context).inflate(layoutId, null)
+        if (layoutId != null) {
+            view = LayoutInflater.from(context).inflate(layoutId, null)
+        } else {
+            view = View(context)
+        }
         val lp = WindowManager.LayoutParams(
             width, height, x, y,
             WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
@@ -51,12 +56,11 @@
             gravity = Gravity.LEFT or Gravity.TOP
             setTrustedOverlay()
         }
-        val wm: WindowManager? = context.getSystemService(WindowManager::class.java)
-        wm?.addView(view, lp)
+        windowManager?.addView(view, lp)
     }
 
     override fun releaseView() {
-        context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view)
+        windowManager?.removeViewImmediate(view)
     }
 
     override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) {
@@ -64,6 +68,6 @@
             this.x = x.toInt()
             this.y = y.toInt()
         }
-        view.layoutParams = lp
+        windowManager?.updateViewLayout(view, lp)
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index 7ade987..6f8e001 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
@@ -18,6 +18,8 @@
 
 import android.app.TaskInfo
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS
 import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
 
@@ -33,5 +35,14 @@
         return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0
     }
 
+/** Whether the task is in fullscreen windowing mode. */
 val TaskInfo.isFullscreen: Boolean
     get() = windowingMode == WINDOWING_MODE_FULLSCREEN
+
+/** Whether the task is in pinned windowing mode. */
+val TaskInfo.isPinned: Boolean
+    get() = windowingMode == WINDOWING_MODE_PINNED
+
+/** Whether the task is in multi-window windowing mode. */
+val TaskInfo.isMultiWindow: Boolean
+    get() = windowingMode == WINDOWING_MODE_MULTI_WINDOW
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 8d822c2..76dfe37 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
@@ -20,37 +20,68 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.res.ColorStateList
 import android.graphics.Color
+import android.graphics.Point
+import android.view.SurfaceControl
 import android.view.View
+import android.view.View.OnClickListener
+import android.view.View.OnHoverListener
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+import android.view.WindowManager
 import android.widget.ImageButton
+import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
 
 /**
  * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
  * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
  */
 internal class AppHandleViewHolder(
-        rootView: View,
-        onCaptionTouchListener: View.OnTouchListener,
-        onCaptionButtonClickListener: View.OnClickListener
+    rootView: View,
+    private val onCaptionTouchListener: View.OnTouchListener,
+    private val onCaptionButtonClickListener: OnClickListener,
+    private val onCaptionHoverListener: OnHoverListener,
 ) : WindowDecorationViewHolder(rootView) {
 
     companion object {
         private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
     }
-
+    private lateinit var taskInfo: RunningTaskInfo
+    private val windowManager = context.getSystemService(WindowManager::class.java)
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
     private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
 
+    // An invisible View that takes up the same coordinates as captionHandle but is layered
+    // above the status bar. The purpose of this View is to receive input intended for
+    // captionHandle.
+    private var statusBarInputLayer: AdditionalSystemViewContainer? = null
+
     init {
         captionView.setOnTouchListener(onCaptionTouchListener)
         captionHandle.setOnTouchListener(onCaptionTouchListener)
         captionHandle.setOnClickListener(onCaptionButtonClickListener)
+        captionHandle.setOnHoverListener(onCaptionHoverListener)
     }
 
-    override fun bindData(taskInfo: RunningTaskInfo) {
+    override fun bindData(
+        taskInfo: RunningTaskInfo,
+        position: Point,
+        width: Int,
+        height: Int,
+        isCaptionVisible: Boolean
+    ) {
         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
+        this.taskInfo = taskInfo
+        if (!isCaptionVisible && hasStatusBarInputLayer()) {
+            disposeStatusBarInputLayer()
+            return
+        }
+        if (hasStatusBarInputLayer()) {
+            updateStatusBarInputLayer(position)
+        } else {
+            createStatusBarInputLayer(position, width, height)
+        }
     }
 
     override fun onHandleMenuOpened() {
@@ -61,6 +92,45 @@
         animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
     }
 
+    private fun createStatusBarInputLayer(handlePosition: Point,
+                                          handleWidth: Int,
+                                          handleHeight: Int) {
+        if (!Flags.enableAdditionalWindowsAboveStatusBar()) return
+        statusBarInputLayer = AdditionalSystemViewContainer(context, taskInfo.taskId,
+            handlePosition.x, handlePosition.y, handleWidth, handleHeight)
+        val view = statusBarInputLayer?.view
+        val lp = view?.layoutParams as WindowManager.LayoutParams
+        lp.title = "Handle Input Layer of task " + taskInfo.taskId
+        lp.setTrustedOverlay()
+        // Make this window a spy window to enable it to pilfer pointers from the system-wide
+        // gesture listener that receives events before window. This is to prevent notification
+        // shade gesture when we swipe down to enter desktop.
+        lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
+        view.id = R.id.caption_handle
+        view.setOnClickListener(onCaptionButtonClickListener)
+        view.setOnTouchListener(onCaptionTouchListener)
+        view.setOnHoverListener(onCaptionHoverListener)
+        windowManager.updateViewLayout(view, lp)
+    }
+
+    private fun updateStatusBarInputLayer(globalPosition: Point) {
+        statusBarInputLayer?.setPosition(SurfaceControl.Transaction(), globalPosition.x.toFloat(),
+            globalPosition.y.toFloat()) ?: return
+    }
+
+    private fun hasStatusBarInputLayer(): Boolean {
+        return statusBarInputLayer != null
+    }
+
+    /**
+     * Remove the input layer from [WindowManager]. Should be used when caption handle
+     * is not visible.
+     */
+    fun disposeStatusBarInputLayer() {
+        statusBarInputLayer?.releaseView()
+        statusBarInputLayer = null
+    }
+
     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
         return if (shouldUseLightCaptionColors(taskInfo)) {
             context.getColor(R.color.desktop_mode_caption_handle_bar_light)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 46127b1..b704d9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -21,6 +21,7 @@
 import android.content.res.Configuration
 import android.graphics.Bitmap
 import android.graphics.Color
+import android.graphics.Point
 import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.RippleDrawable
 import android.graphics.drawable.ShapeDrawable
@@ -136,7 +137,13 @@
                 onMaximizeHoverAnimationFinishedListener
     }
 
-    override fun bindData(taskInfo: RunningTaskInfo) {
+    override fun bindData(
+        taskInfo: RunningTaskInfo,
+        position: Point,
+        width: Int,
+        height: Int,
+        isCaptionVisible: Boolean
+    ) {
         if (Flags.enableThemedAppHeaders()) {
             bindDataWithThemedHeaders(taskInfo)
         } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 5ae8d25..2341b09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -17,6 +17,7 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
+import android.graphics.Point
 import android.view.View
 
 /**
@@ -30,7 +31,13 @@
    * A signal to the view holder that new data is available and that the views should be updated to
    * reflect it.
    */
-  abstract fun bindData(taskInfo: RunningTaskInfo)
+  abstract fun bindData(
+    taskInfo: RunningTaskInfo,
+    position: Point,
+    width: Int,
+    height: Int,
+    isCaptionVisible: Boolean
+  )
 
   /** Callback when the handle menu is opened. */
   abstract fun onHandleMenuOpened()
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt
new file mode 100644
index 0000000..b4cadf4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/SwitchToOverviewFromDesktop.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+/**
+* Base test for opening recent apps overview from desktop mode.
+*
+* Navigation mode can be passed as a constructor parameter, by default it is set to gesture navigation.
+*/
+@Ignore("Base Test Class")
+abstract class SwitchToOverviewFromDesktop
+@JvmOverloads
+constructor(val navigationMode: NavBar = NavBar.MODE_GESTURAL) {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val tapl = LauncherInstrumentation()
+    private val wmHelper = WindowManagerStateHelper(instrumentation)
+    private val device = UiDevice.getInstance(instrumentation)
+    private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+
+    @Rule @JvmField val testSetupRule = Utils.testSetupRule(navigationMode, Rotation.ROTATION_0)
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        testApp.enterDesktopWithDrag(wmHelper, device)
+    }
+
+    @Test
+    open fun switchToOverview() {
+        tapl.getLaunchedAppState().switchToOverview()
+    }
+
+    @After
+    fun teardown() {
+        testApp.exit(wmHelper)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index db962e7..2406bde 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -48,7 +48,10 @@
 
     @Before
     fun setup() {
-        tapl.workspace.switchToOverview().dismissAllTasks()
+        val overview = tapl.workspace.switchToOverview()
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks()
+        }
 
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 6b69542..a040865 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -37,6 +37,7 @@
     ],
 
     static_libs: [
+        "TestParameterInjector",
         "WindowManager-Shell",
         "junit",
         "flag-junit",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 55b6bd2..bba9418 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -46,12 +46,14 @@
 import android.view.animation.Animation;
 import android.window.TransitionInfo;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.window.flags.Flags;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
 
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -59,6 +61,7 @@
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * Tests for {@link ActivityEmbeddingAnimationRunner}.
@@ -67,7 +70,7 @@
  *  atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
  */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(TestParameterInjector.class)
 public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
 
     @Rule
@@ -204,15 +207,13 @@
     // TODO(b/243518738): Rewrite with TestParameter
     @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG)
     @Test
-    public void testCalculateParentBounds_flagEnabled() {
+    public void testCalculateParentBounds_flagEnabled_emptyParentSize() {
         TransitionInfo.Change change;
         final TransitionInfo.Change stubChange = createChange(0 /* flags */);
         final Rect actualParentBounds = new Rect();
-        Rect parentBounds = new Rect(0, 0, 2000, 2000);
-        Rect endAbsBounds = new Rect(0, 0, 2000, 2000);
         change = prepareChangeForParentBoundsCalculationTest(
                 new Point(0, 0) /* endRelOffset */,
-                endAbsBounds,
+                new Rect(0, 0, 2000, 2000),
                 new Point() /* endParentSize */
         );
 
@@ -220,69 +221,80 @@
 
         assertTrue("Parent bounds must be empty because end parent size is not set.",
                 actualParentBounds.isEmpty());
+    }
 
-        String testString = "Parent start with (0, 0)";
-        change = prepareChangeForParentBoundsCalculationTest(
+    @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG)
+    @Test
+    public void testCalculateParentBounds_flagEnabled(
+            @TestParameter ParentBoundsTestParameters params) {
+        final TransitionInfo.Change stubChange = createChange(0 /*flags*/);
+        final Rect parentBounds = params.getParentBounds();
+        final Rect endAbsBounds = params.getEndAbsBounds();
+        final TransitionInfo.Change change = prepareChangeForParentBoundsCalculationTest(
                 new Point(endAbsBounds.left - parentBounds.left,
                         endAbsBounds.top - parentBounds.top),
                 endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+        final Rect actualParentBounds = new Rect();
 
         calculateParentBounds(change, stubChange, actualParentBounds);
 
-        assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
-                actualParentBounds);
+        assertEquals(parentBounds, actualParentBounds);
+    }
 
-        testString = "Container not start with (0, 0)";
-        parentBounds = new Rect(0, 0, 2000, 2000);
-        endAbsBounds = new Rect(1000, 500, 2000, 1500);
-        change = prepareChangeForParentBoundsCalculationTest(
-                new Point(endAbsBounds.left - parentBounds.left,
-                        endAbsBounds.top - parentBounds.top),
-                endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+    private enum ParentBoundsTestParameters {
+        PARENT_START_WITH_0_0(
+                new int[]{0, 0, 2000, 2000},
+                new int[]{0, 0, 2000, 2000}),
+        CONTAINER_NOT_START_WITH_0_0(
+                new int[] {0, 0, 2000, 2000},
+                new int[] {1000, 500, 1500, 1500}),
+        PARENT_ON_THE_RIGHT(
+                new int[] {1000, 0, 2000, 2000},
+                new int[] {1000, 500, 1500, 1500}),
+        PARENT_ON_THE_BOTTOM(
+                new int[] {0, 1000, 2000, 2000},
+                new int[] {500, 1500, 1500, 2000}),
+        PARENT_IN_THE_MIDDLE(
+                new int[] {500, 500, 1500, 1500},
+                new int[] {1000, 500, 1500, 1000});
 
-        calculateParentBounds(change, stubChange, actualParentBounds);
+        /**
+         * An int array to present {left, top, right, bottom} of the parent {@link Rect bounds}.
+         */
+        @NonNull
+        private final int[] mParentBounds;
 
-        assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
-                actualParentBounds);
+        /**
+         * An int array to present {left, top, right, bottom} of the absolute container
+         * {@link Rect bounds} after the transition finishes.
+         */
+        @NonNull
+        private final int[] mEndAbsBounds;
 
-        testString = "Parent container on the right";
-        parentBounds = new Rect(1000, 0, 2000, 2000);
-        endAbsBounds = new Rect(1000, 500, 1500, 1500);
-        change = prepareChangeForParentBoundsCalculationTest(
-                new Point(endAbsBounds.left - parentBounds.left,
-                        endAbsBounds.top - parentBounds.top),
-                endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+        ParentBoundsTestParameters(
+                @NonNull int[] parentBounds, @NonNull int[] endAbsBounds) {
+            mParentBounds = parentBounds;
+            mEndAbsBounds = endAbsBounds;
+        }
 
-        calculateParentBounds(change, stubChange, actualParentBounds);
+        @NonNull
+        private Rect getParentBounds() {
+            return asRect(mParentBounds);
+        }
 
-        assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
-                actualParentBounds);
+        @NonNull
+        private Rect getEndAbsBounds() {
+            return asRect(mEndAbsBounds);
+        }
 
-        testString = "Parent container on the bottom";
-        parentBounds = new Rect(0, 1000, 2000, 2000);
-        endAbsBounds = new Rect(500, 1500, 1500, 2000);
-        change = prepareChangeForParentBoundsCalculationTest(
-                new Point(endAbsBounds.left - parentBounds.left,
-                        endAbsBounds.top - parentBounds.top),
-                endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
-
-        calculateParentBounds(change, stubChange, actualParentBounds);
-
-        assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
-                actualParentBounds);
-
-        testString = "Parent container in the middle";
-        parentBounds = new Rect(500, 500, 1500, 1500);
-        endAbsBounds = new Rect(1000, 500, 1500, 1000);
-        change = prepareChangeForParentBoundsCalculationTest(
-                new Point(endAbsBounds.left - parentBounds.left,
-                        endAbsBounds.top - parentBounds.top),
-                endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
-
-        calculateParentBounds(change, stubChange, actualParentBounds);
-
-        assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
-                actualParentBounds);
+        @NonNull
+        private static Rect asRect(@NonNull int[] bounds) {
+            if (bounds.length != 4) {
+                throw new IllegalArgumentException("There must be exactly 4 elements in bounds, "
+                        + "but found " + bounds.length + ": " + Arrays.toString(bounds));
+            }
+            return new Rect(bounds[0], bounds[1], bounds[2], bounds[3]);
+        }
     }
 
     @Test
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 da88686..8421365 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
@@ -580,138 +580,6 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask()
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
-            shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
-    val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
   fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
     val task = setUpFullscreenTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -1326,6 +1194,22 @@
   }
 
   @Test
+  fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
+    assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+    val freeformTask = setUpFreeformTask()
+    markTaskHidden(freeformTask)
+
+    val wct =
+      controller.handleRequest(Binder(), createTransition(freeformTask))
+
+    // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA.
+    assertNotNull(wct, "should handle request")
+    assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
+      .isEqualTo(WINDOWING_MODE_UNDEFINED)
+  }
+
+  @Test
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
     assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -1574,7 +1458,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
   )
-  fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -1587,7 +1471,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() {
+  fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -1598,7 +1482,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() {
+  fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -1611,7 +1495,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
@@ -1625,7 +1509,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -1640,7 +1524,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -1656,7 +1540,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1671,7 +1555,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() {
+  fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1684,7 +1568,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasks_backNavigationDisabled_doesNotHandle() {
+  fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1699,7 +1583,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1716,7 +1600,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1734,7 +1618,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1751,7 +1635,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1769,7 +1653,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_backTransition_nonMinimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() {
+  fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1789,7 +1673,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -1802,7 +1686,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() {
+  fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -1813,7 +1697,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() {
+  fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -1826,7 +1710,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
     val task = setUpFreeformTask()
 
     desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
@@ -1840,7 +1724,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -1855,7 +1739,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() {
     val task = setUpFreeformTask()
     val wallpaperToken = MockToken().token()
 
@@ -1871,7 +1755,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() {
+  fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1886,7 +1770,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() {
+  fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1900,7 +1784,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksFlagEnabled_backNavigationDisabled_doesNotHandle() {
+  fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() {
     val task1 = setUpFreeformTask()
     setUpFreeformTask()
 
@@ -1915,7 +1799,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1932,7 +1816,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1950,7 +1834,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_multipleTasksOneNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() {
+  fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1967,7 +1851,7 @@
   @Test
   @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
   @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
-  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() {
+  fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
@@ -1985,7 +1869,7 @@
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
     Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
   )
-  fun handleRequest_closeTransition_minimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() {
+  fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() {
     val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
     val wallpaperToken = MockToken().token()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 6888de5..75d2145 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -182,7 +182,7 @@
 
     @Test
     public void instantiatePipController_registersPipTransitionCallback() {
-        verify(mMockPipTransitionController).registerPipTransitionCallback(any());
+        verify(mMockPipTransitionController).registerPipTransitionCallback(any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
index ace09a8..66f8c0b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipResizeGestureHandlerTest.java
@@ -114,8 +114,8 @@
         final PipBoundsAlgorithm pipBoundsAlgorithm = new PipBoundsAlgorithm(mContext,
                 mPipBoundsState, pipSnapAlgorithm, pipKeepClearAlgorithm, mPipDisplayLayoutState,
                 mSizeSpecSource);
-        final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mPipBoundsState,
-                mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
+        final PipMotionHelper motionHelper = new PipMotionHelper(mContext, mMainExecutor,
+                mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, pipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator,
                 Optional.empty() /* pipPerfHintControllerOptional */);
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 92762fa..6d18e36 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -116,8 +116,8 @@
         mPipSnapAlgorithm = new PipSnapAlgorithm();
         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, mPipSnapAlgorithm,
                 new PipKeepClearAlgorithmInterface() {}, mPipDisplayLayoutState, mSizeSpecSource);
-        PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
-                mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
+        PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mMainExecutor,
+                mPipBoundsState, mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator,
                 Optional.empty() /* pipPerfHintControllerOptional */);
         mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 5b95b15..1c5d5e9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -50,7 +50,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.Bundle;
-import android.os.IBinder;
+import android.os.Handler;
 import android.window.IWindowContainerToken;
 import android.window.WindowContainerToken;
 
@@ -104,6 +104,7 @@
     @Mock SyncTransactionQueue mSyncQueue;
     @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
     @Mock ShellExecutor mMainExecutor;
+    @Mock Handler mMainHandler;
     @Mock DisplayController mDisplayController;
     @Mock DisplayImeController mDisplayImeController;
     @Mock DisplayInsetsController mDisplayInsetsController;
@@ -134,7 +135,7 @@
                 mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
                 mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController,
                 Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController),
-                mStageCoordinator, mMultiInstanceHelper, mMainExecutor));
+                mStageCoordinator, mMultiInstanceHelper, mMainExecutor, mMainHandler));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index a3009a5..29d3fb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -22,6 +22,7 @@
 import android.app.ActivityManager;
 import android.content.Context;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 
@@ -77,13 +78,13 @@
                 DisplayController displayController, DisplayImeController imeController,
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
-                ShellExecutor mainExecutor,
+                ShellExecutor mainExecutor, Handler mainHandler,
                 Optional<RecentTasksController> recentTasks,
                 LaunchAdjacentController launchAdjacentController,
                 Optional<WindowDecorViewModel> windowDecorViewModel) {
             super(context, displayId, syncQueue, taskOrganizer, mainStage,
                     sideStage, displayController, imeController, insetsController, splitLayout,
-                    transitions, transactionPool, mainExecutor, recentTasks,
+                    transitions, transactionPool, mainExecutor, mainHandler, recentTasks,
                     launchAdjacentController, windowDecorViewModel);
 
             // Prepare root task for testing.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 34b2eeb..37ef788 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -49,6 +49,7 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.os.Handler;
 import android.os.IBinder;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
@@ -107,6 +108,7 @@
     @Mock private IconProvider mIconProvider;
     @Mock private WindowDecorViewModel mWindowDecorViewModel;
     @Mock private ShellExecutor mMainExecutor;
+    @Mock private Handler mMainHandler;
     @Mock private LaunchAdjacentController mLaunchAdjacentController;
     @Mock private DefaultMixedHandler mMixedHandler;
     @Mock private SplitScreen.SplitInvocationListener mInvocationListener;
@@ -140,7 +142,7 @@
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
-                mTransactionPool, mMainExecutor, Optional.empty(),
+                mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(),
                 mLaunchAdjacentController, Optional.empty());
         mStageCoordinator.setMixedHandler(mMixedHandler);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index d18fec2..eaef704 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -138,7 +138,8 @@
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
-                mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty()));
+                mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController,
+                Optional.empty()));
         mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
 
         when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
@@ -347,8 +348,7 @@
 
         assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token);
         assertThat(options.getPendingIntentBackgroundActivityStartMode())
-                .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
-        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue();
+                .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
     }
 
     @Test
@@ -359,10 +359,11 @@
         mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build();
         when(mStageCoordinator.isSplitActive()).thenReturn(true);
         when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true);
+        when(mStageCoordinator.willSleepOnFold()).thenReturn(true);
 
         mStageCoordinator.onFoldedStateChanged(true);
 
-        assertEquals(mStageCoordinator.mTopStageAfterFoldDismiss, STAGE_TYPE_MAIN);
+        assertEquals(mStageCoordinator.mLastActiveStage, STAGE_TYPE_MAIN);
 
         mStageCoordinator.onFinishedWakingUp();
 
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 b1803e9..aeae0be 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.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
 import android.os.Handler
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.CheckFlagsRule
@@ -62,6 +63,7 @@
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.DisplayController
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
@@ -71,6 +73,7 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.sysui.KeyguardChangeListener
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
@@ -78,8 +81,6 @@
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
 import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
-import java.util.Optional
-import java.util.function.Supplier
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -99,6 +100,8 @@
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
+import java.util.Optional
+import java.util.function.Supplier
 
 /**
  * Tests of [DesktopModeWindowDecorViewModel]
@@ -122,6 +125,7 @@
     @Mock private lateinit var mockMainChoreographer: Choreographer
     @Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
     @Mock private lateinit var mockDisplayController: DisplayController
+    @Mock private lateinit var mockSplitScreenController: SplitScreenController
     @Mock private lateinit var mockDisplayLayout: DisplayLayout
     @Mock private lateinit var displayInsetsController: DisplayInsetsController
     @Mock private lateinit var mockSyncQueue: SyncTransactionQueue
@@ -136,6 +140,7 @@
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
     @Mock private lateinit var mockWindowManager: IWindowManager
     @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
+    private val bgExecutor = TestShellExecutor()
 
     private val transactionFactory = Supplier<SurfaceControl.Transaction> {
         SurfaceControl.Transaction()
@@ -155,6 +160,7 @@
                 mockShellExecutor,
                 mockMainHandler,
                 mockMainChoreographer,
+                bgExecutor,
                 shellInit,
                 mockShellCommandHandler,
                 mockWindowManager,
@@ -171,7 +177,7 @@
                 mockRootTaskDisplayAreaOrganizer,
             windowDecorByTaskIdSpy, mockInteractionJankMonitor
         )
-
+        desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
         whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
         whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
         whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor)
@@ -204,10 +210,12 @@
         verify(mockDesktopModeWindowDecorFactory).create(
                 mContext,
                 mockDisplayController,
+                mockSplitScreenController,
                 mockTaskOrganizer,
                 task,
                 taskSurface,
                 mockMainHandler,
+                bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
                 mockRootTaskDisplayAreaOrganizer
@@ -228,10 +236,12 @@
         verify(mockDesktopModeWindowDecorFactory, never()).create(
                 mContext,
                 mockDisplayController,
+                mockSplitScreenController,
                 mockTaskOrganizer,
                 task,
                 taskSurface,
                 mockMainHandler,
+                bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
                 mockRootTaskDisplayAreaOrganizer
@@ -243,10 +253,12 @@
         verify(mockDesktopModeWindowDecorFactory, times(1)).create(
                 mContext,
                 mockDisplayController,
+                mockSplitScreenController,
                 mockTaskOrganizer,
                 task,
                 taskSurface,
                 mockMainHandler,
+                bgExecutor,
                 mockMainChoreographer,
                 mockSyncQueue,
                 mockRootTaskDisplayAreaOrganizer
@@ -254,6 +266,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testCreateAndDisposeEventReceiver() {
         val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
         setUpMockDecorationForTask(task)
@@ -266,6 +279,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testEventReceiversOnMultipleDisplays() {
         val secondaryDisplay = createVirtualDisplay() ?: return
         val secondaryDisplayId = secondaryDisplay.display.displayId
@@ -344,7 +358,8 @@
         onTaskChanging(task)
 
         verify(mockDesktopModeWindowDecorFactory, never())
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                    any())
     }
 
     @Test
@@ -365,7 +380,8 @@
 
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
-                    .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                    .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+                        any(), any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -382,7 +398,8 @@
         onTaskOpening(task)
 
         verify(mockDesktopModeWindowDecorFactory, never())
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                    any())
     }
 
     @Test
@@ -399,7 +416,8 @@
         onTaskOpening(task)
 
         verify(mockDesktopModeWindowDecorFactory, never())
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+                    any(), any())
     }
 
     @Test
@@ -496,7 +514,8 @@
 
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory, never())
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                    any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -520,7 +539,8 @@
 
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                    any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -543,7 +563,8 @@
 
             onTaskOpening(task)
             verify(mockDesktopModeWindowDecorFactory)
-                .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+                any())
         } finally {
             mockitoSession.finishMocking()
         }
@@ -682,7 +703,7 @@
         val decoration = mock(DesktopModeWindowDecoration::class.java)
         whenever(
             mockDesktopModeWindowDecorFactory.create(
-                any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+                any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
         ).thenReturn(decoration)
         decoration.mTaskInfo = task
         whenever(decoration.isFocused).thenReturn(task.isFocused)
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 d860609..412fef3 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
@@ -82,9 +82,12 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
 import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
 
@@ -127,6 +130,8 @@
     @Mock
     private DisplayController mMockDisplayController;
     @Mock
+    private SplitScreenController mMockSplitScreenController;
+    @Mock
     private ShellTaskOrganizer mMockShellTaskOrganizer;
     @Mock
     private Choreographer mMockChoreographer;
@@ -165,6 +170,7 @@
     private SurfaceControl.Transaction mMockTransaction;
     private StaticMockitoSession mMockitoSession;
     private TestableContext mTestableContext;
+    private ShellExecutor mBgExecutor = new TestShellExecutor();
 
     /** Set up run before test class. */
     @BeforeClass
@@ -392,6 +398,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     public void relayout_fullscreenTask_appliesTransactionImmediately() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -418,6 +425,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -429,6 +437,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     public void relayout_fullscreenTask_postsViewHostCreation() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -457,6 +466,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     public void relayout_removesExistingHandlerCallback() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -471,6 +481,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     public void close_removesExistingHandlerCallback() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -656,8 +667,9 @@
             ActivityManager.RunningTaskInfo taskInfo,
             MaximizeMenuFactory maximizeMenuFactory) {
         final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
-                mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
-                mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+                mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer,
+                taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer,
+                mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
                 SurfaceControl.Builder::new, mMockTransactionSupplier,
                 WindowContainerTransaction::new, SurfaceControl::new,
                 mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
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 adda9a6..e548f8f 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
@@ -130,7 +130,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testFullscreenMenuUsesSystemViewContainer() {
         createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED)
-        val handleMenu = createAndShowHandleMenu()
+        val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of display.
@@ -142,7 +142,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testFreeformMenu_usesViewHostViewContainer() {
         createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED)
-        handleMenu = createAndShowHandleMenu()
+        handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer)
         // Verify menu is created near top-left of task.
         val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN)
@@ -153,7 +153,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testSplitLeftMenu_usesSystemViewContainer() {
         createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT)
-        handleMenu = createAndShowHandleMenu()
+        handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT)
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of split left task.
@@ -168,7 +168,7 @@
     @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
     fun testSplitRightMenu_usesSystemViewContainer() {
         createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT)
-        handleMenu = createAndShowHandleMenu()
+        handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT)
         assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
         // Verify menu is created at coordinates that, when added to WindowManager,
         // show at the top-center of split right task.
@@ -208,16 +208,30 @@
         }
     }
 
-    private fun createAndShowHandleMenu(): HandleMenu {
+    private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu {
         val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
             R.layout.desktop_mode_app_header
         } else {
             R.layout.desktop_mode_app_handle
         }
+        val captionX = when (mockDesktopWindowDecoration.mTaskInfo.windowingMode) {
+            WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+            WINDOWING_MODE_FREEFORM -> 0
+            WINDOWING_MODE_MULTI_WINDOW -> {
+                if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                    (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+                } else {
+                    (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
+                }
+            }
+            else -> error("Invalid windowing mode")
+        }
         val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
                 onClickListener, onTouchListener, appIcon, appName, displayController,
-                splitScreenController, true /* shouldShowWindowingPill */,
-                true /* shouldShowBrowserPill */, 50 /* captionHeight */)
+                splitScreenController, shouldShowWindowingPill = true,
+                shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+                captionX = captionX
+        )
         handleMenu.show()
         return handleMenu
     }
@@ -233,5 +247,6 @@
         private const val MENU_START_MARGIN = 20
         private const val MENU_PILL_ELEVATION = 2
         private const val MENU_PILL_SPACING_MARGIN = 4
+        private const val HANDLE_WIDTH = 80
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
index d3e996b..3b49055 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
@@ -68,12 +68,12 @@
     fun testReleaseView_ViewRemoved() {
         viewContainer = AdditionalSystemViewContainer(
             mockContext,
-            R.layout.desktop_mode_window_decor_handle_menu,
             TASK_ID,
             X,
             Y,
             WIDTH,
-            HEIGHT
+            HEIGHT,
+            R.layout.desktop_mode_window_decor_handle_menu
         )
         verify(mockWindowManager).addView(eq(mockView), any())
         viewContainer.releaseView()
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index acfe473..0edaaef 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -2,6 +2,20 @@
 container: "system"
 
 flag {
+    name: "keep_gnss_stationary_throttling"
+    namespace: "location"
+    description: "Keeps stationary throttling for the GNSS provider even if the disable_stationary_throttling flag is true."
+    bug: "354000147"
+}
+
+flag {
+    name: "disable_stationary_throttling"
+    namespace: "location"
+    description: "Disables stationary throttling for all providers"
+    bug: "354000147"
+}
+
+flag {
     name: "new_geocoder"
     namespace: "location"
     description: "Flag for new Geocoder APIs"
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 2d0e7ab..a255f73 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2651,4 +2651,11 @@
      * @hide
      */
     public static native boolean isBluetoothVariableLatencyEnabled();
+
+    /**
+     * Register a native listener for system property sysprop
+     * @param callback the listener which fires when the property changes
+     * @hide
+     */
+    public static native void listenForSystemPropertyChange(String sysprop, Runnable callback);
 }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 999f40e5..1c5049e 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -23,6 +23,7 @@
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.VirtualDisplayFlag;
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.os.Build;
@@ -140,6 +141,7 @@
     /**
      * @hide
      */
+    @Nullable
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
             int width, int height, int dpi, boolean isSecure, @Nullable Surface surface,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
@@ -192,6 +194,11 @@
      *                                 <li>If attempting to create a new virtual display
      *                                 associated with this MediaProjection instance after it has
      *                                 been stopped by invoking {@link #stop()}.
+     *                                 <li>If attempting to create a new virtual display
+     *                                 associated with this MediaProjection instance after a
+     *                                 {@link MediaProjection.Callback#onStop()} callback has been
+     *                                 received due to the user or the system stopping the
+     *                                 MediaProjection session.
      *                                 <li>If the target SDK is {@link
      *                                 android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up,
      *                                 and if this instance has already taken a recording through
@@ -208,12 +215,17 @@
      *                               {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}.
      *                               Instead, recording doesn't begin until the user re-grants
      *                               consent in the dialog.
+     * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay}
+     * could be created.
      * @see VirtualDisplay
      * @see VirtualDisplay.Callback
      */
+    @SuppressWarnings("RequiresPermission")
+    @Nullable
     public VirtualDisplay createVirtualDisplay(@NonNull String name,
-            int width, int height, int dpi, int flags, @Nullable Surface surface,
-            @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
+            int width, int height, int dpi, @VirtualDisplayFlag int flags,
+            @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback,
+            @Nullable Handler handler) {
         if (shouldMediaProjectionRequireCallback()) {
             if (mCallbacks.isEmpty()) {
                 final IllegalStateException e = new IllegalStateException(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 88770d4..186b69b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -912,8 +912,10 @@
                     "message: $message"
             )
         }
+
+        val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
+
         if (statusCode == PackageInstaller.STATUS_SUCCESS) {
-            val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
             val resultIntent = if (shouldReturnResult) {
                 Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED)
             } else {
@@ -922,12 +924,34 @@
             }
             _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
         } else {
-            if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) {
+            // TODO (b/346655018): Use INSTALL_FAILED_ABORTED legacyCode in the condition
+            // statusCode can be STATUS_FAILURE_ABORTED if:
+            // 1. GPP blocks an install.
+            // 2. User denies ownership update explicitly.
+            // InstallFailed dialog must not be shown only when the user denies ownership update. We
+            // must show this dialog for all other install failures.
+
+            val userDenied =
+                    statusCode == PackageInstaller.STATUS_FAILURE_ABORTED &&
+                    legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_TIMEOUT &&
+                    legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE
+
+            if (shouldReturnResult) {
+                val resultIntent = Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus)
                 _installResult.setValue(
-                    InstallFailed(appSnippet, statusCode, legacyStatus, message)
+                    InstallFailed(
+                        legacyCode = legacyStatus,
+                        statusCode = statusCode,
+                        shouldReturnResult = true,
+                        resultIntent = resultIntent
+                    )
                 )
-            } else {
+            } else if (userDenied) {
                 _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR))
+            } else {
+                _installResult.setValue(
+                    InstallFailed(appSnippet, legacyStatus, statusCode, message)
+                )
             }
         }
     }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
index 5dd4d29..8de8fbb 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.content.pm.PackageInstaller
 import android.graphics.drawable.Drawable
 
 sealed class InstallStage(val stageCode: Int) {
@@ -77,11 +78,10 @@
     val shouldReturnResult: Boolean = false,
     /**
      *
-     * * If the caller is requesting a result back, this will hold the Intent with
-     * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent
-     * back to the caller.
+     * * If the caller is requesting a result back, this will hold an Intent with
+     * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED].
      *
-     * * If the caller doesn't want the result back, this will hold the Intent that launches
+     * * If the caller doesn't want the result back, this will hold an Intent that launches
      * the newly installed / updated app if a launchable activity exists.
      */
     val resultIntent: Intent? = null,
@@ -95,17 +95,23 @@
 }
 
 data class InstallFailed(
-    private val appSnippet: PackageUtil.AppSnippet,
+    private val appSnippet: PackageUtil.AppSnippet? = null,
     val legacyCode: Int,
     val statusCode: Int,
-    val message: String?,
+    val message: String? = null,
+    val shouldReturnResult: Boolean = false,
+    /**
+     * If the caller is requesting a result back, this will hold an Intent with
+     * [Intent.EXTRA_INSTALL_RESULT] set to the [PackageInstaller.EXTRA_LEGACY_STATUS].
+     */
+    val resultIntent: Intent? = null
 ) : InstallStage(STAGE_FAILED) {
 
     val appIcon: Drawable?
-        get() = appSnippet.icon
+        get() = appSnippet?.icon
 
     val appLabel: String?
-        get() = appSnippet.label as String?
+        get() = appSnippet?.label as String?
 }
 
 data class InstallAborted(
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index 31b9ccb..e2ab316 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -171,15 +171,20 @@
                     val successIntent = success.resultIntent
                     setResult(Activity.RESULT_OK, successIntent, true)
                 } else {
-                    val successFragment = InstallSuccessFragment(success)
-                    showDialogInner(successFragment)
+                    val successDialog = InstallSuccessFragment(success)
+                    showDialogInner(successDialog)
                 }
             }
 
             InstallStage.STAGE_FAILED -> {
                 val failed = installStage as InstallFailed
-                val failedDialog = InstallFailedFragment(failed)
-                showDialogInner(failedDialog)
+                if (failed.shouldReturnResult) {
+                    val failureIntent = failed.resultIntent
+                    setResult(Activity.RESULT_FIRST_USER, failureIntent, true)
+                } else {
+                    val failureDialog = InstallFailedFragment(failed)
+                    showDialogInner(failureDialog)
+                }
             }
 
             else -> {
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index 6df0e99..ac44a1b 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -25,9 +25,6 @@
     use_resource_processor: true,
     static_libs: [
         "SettingsLibColor",
-        "androidx.slice_slice-builders",
-        "androidx.slice_slice-core",
-        "androidx.slice_slice-view",
         "androidx.compose.animation_animation",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 9b8ecf7..492d7c0 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -54,9 +54,6 @@
 dependencies {
     api(project(":SettingsLibColor"))
     api("androidx.appcompat:appcompat:1.7.0-rc01")
-    api("androidx.slice:slice-builders:1.1.0-alpha02")
-    api("androidx.slice:slice-core:1.1.0-alpha02")
-    api("androidx.slice:slice-view:1.1.0-alpha02")
     api("androidx.compose.material3:material3:1.3.0-beta02")
     api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
     api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
@@ -97,10 +94,6 @@
 
                     // Excludes debug functions
                     "com/android/settingslib/spa/framework/compose/TimeMeasurer*",
-
-                    // Excludes slice demo presenter & provider
-                    "com/android/settingslib/spa/slice/presenter/Demo*",
-                    "com/android/settingslib/spa/slice/provider/Demo*",
                 )
             )
         }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
deleted file mode 100644
index fc551a8..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2022 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.settingslib.spa.framework.common
-
-import androidx.lifecycle.LiveData
-import androidx.slice.Slice
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
-open class EntrySliceData : LiveData<Slice?>() {
-    private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
-    private var asyncRunnerJob: Job? = null
-    private var asyncActionJob: Job? = null
-    private var isActive = false
-
-    open suspend fun asyncRunner() {}
-
-    open suspend fun asyncAction() {}
-
-    override fun onActive() {
-        asyncRunnerJob?.cancel()
-        asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
-        isActive = true
-    }
-
-    override fun onInactive() {
-        asyncRunnerJob?.cancel()
-        asyncRunnerJob = null
-        asyncActionJob?.cancel()
-        asyncActionJob = null
-        isActive = false
-    }
-
-    fun isActive(): Boolean {
-        return isActive
-    }
-
-    fun doAction() {
-        asyncActionJob?.cancel()
-        asyncActionJob = asyncRunnerScope.launch { asyncAction() }
-    }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 90581b9..3f53091 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.spa.framework.common
 
-import android.net.Uri
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -42,7 +41,6 @@
 typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
 typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
 typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
-typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
 
 /**
  * Defines data of a Settings entry.
@@ -80,9 +78,6 @@
     // If so, for instance, we'll reindex its status for search.
     val hasMutableStatus: Boolean = false,
 
-    // Indicate whether the entry has SliceProvider support.
-    val hasSliceSupport: Boolean = false,
-
     /**
      * ========================================
      * Defines entry APIs to get data here.
@@ -102,12 +97,6 @@
     private val searchDataImpl: SearchDataGetter = { null },
 
     /**
-     * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
-     * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
-     */
-    private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
-
-    /**
      * API to Render UI of this entry directly. For now, we use it in the internal injection, to
      * support the case that the injection page owner wants to maintain both data and UI of the
      * injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
@@ -137,10 +126,6 @@
         return searchDataImpl(fullArgument(runtimeArguments))
     }
 
-    fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
-        return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
-    }
-
     @Composable
     fun UiLayout(runtimeArguments: Bundle? = null) {
         val arguments = remember { fullArgument(runtimeArguments) }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
index 0d489e8..ddb571d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.spa.framework.common
 
-import android.net.Uri
 import android.os.Bundle
 import androidx.compose.runtime.remember
 import com.android.settingslib.spa.framework.util.genEntryId
@@ -36,13 +35,11 @@
     private var isAllowSearch: Boolean = false
     private var isSearchDataDynamic: Boolean = false
     private var hasMutableStatus: Boolean = false
-    private var hasSliceSupport: Boolean = false
 
     // Functions
     private var uiLayoutFn: UiLayerRenderer = { }
     private var statusDataFn: StatusDataGetter = { null }
     private var searchDataFn: SearchDataGetter = { null }
-    private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
 
     fun build(): SettingsEntry {
         val page = fromPage ?: owner
@@ -58,16 +55,13 @@
             toPage = toPage,
 
             // attributes
-            // TODO: set isEnabled & (isAllowSearch, hasSliceSupport) separately
             isAllowSearch = isEnabled && isAllowSearch,
             isSearchDataDynamic = isSearchDataDynamic,
             hasMutableStatus = hasMutableStatus,
-            hasSliceSupport = isEnabled && hasSliceSupport,
 
             // functions
             statusDataImpl = statusDataFn,
             searchDataImpl = searchDataFn,
-            sliceDataImpl = sliceDataFn,
             uiLayoutImpl = uiLayoutFn,
         )
     }
@@ -123,12 +117,6 @@
         return this
     }
 
-    fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
-        this.sliceDataFn = fn
-        this.hasSliceSupport = true
-        return this
-    }
-
     fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
         this.uiLayoutFn = fn
         return this
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 95c7d23..cc5351a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -57,8 +57,8 @@
     /**
      * The API to indicate whether the page is enabled or not.
      * During SPA page migration, one can use it to enable certain pages in one release.
-     * When the page is disabled, all its related functionalities, such as browsing, search,
-     * slice provider, are disabled as well.
+     * When the page is disabled, all its related functionalities, such as browsing and search,
+     * are disabled as well.
      */
     fun isEnabled(arguments: Bundle?): Boolean = true
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 6e5132b..11ae9e9 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -74,7 +74,7 @@
     // Set your SpaLogger implementation, for any SPA events logging.
     open val logger: SpaLogger = object : SpaLogger {}
 
-    // Specify class name of browse activity and slice broadcast receiver, which is used to
+    // Specify class name of browse activity, which is used to
     // generate the necessary intents.
     open val browseActivityClass: Class<out Activity>? = null
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt
index d8c35a3..9e8ca0c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt
@@ -25,7 +25,6 @@
 const val SESSION_UNKNOWN = "unknown"
 const val SESSION_BROWSE = "browse"
 const val SESSION_SEARCH = "search"
-const val SESSION_SLICE = "slice"
 const val SESSION_EXTERNAL = "external"
 
 const val KEY_DESTINATION = "spaActivityDestination"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
deleted file mode 100644
index ec89c7c..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 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.settingslib.spa.slice
-
-import android.net.Uri
-import android.os.Bundle
-import com.android.settingslib.spa.framework.util.KEY_DESTINATION
-import com.android.settingslib.spa.framework.util.KEY_HIGHLIGHT_ENTRY
-
-// Defines SliceUri, which contains special query parameters:
-//  -- KEY_DESTINATION: The route that this slice is navigated to.
-//  -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
-//  Other parameters can considered as runtime parameters.
-// Use {entryId, runtimeParams} as the unique Id of this Slice.
-typealias SliceUri = Uri
-
-fun SliceUri.getEntryId(): String? {
-    return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
-}
-
-fun Uri.Builder.appendSpaParams(
-    destination: String? = null,
-    entryId: String? = null,
-    runtimeArguments: Bundle? = null
-): Uri.Builder {
-    if (destination != null) appendQueryParameter(KEY_DESTINATION, destination)
-    if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
-    if (runtimeArguments != null) {
-        for (key in runtimeArguments.keySet()) {
-            appendQueryParameter(key, runtimeArguments.getString(key, ""))
-        }
-    }
-    return this
-}
-
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index ce34979..d0a040c 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -17,7 +17,6 @@
 package com.android.settingslib.spa.framework.common
 
 import android.content.Context
-import android.net.Uri
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.core.os.bundleOf
@@ -25,8 +24,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settingslib.spa.framework.util.genEntryId
 import com.android.settingslib.spa.framework.util.genPageId
-import com.android.settingslib.spa.slice.appendSpaParams
-import com.android.settingslib.spa.slice.getEntryId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
@@ -76,7 +73,6 @@
         assertThat(entry.isAllowSearch).isFalse()
         assertThat(entry.isSearchDataDynamic).isFalse()
         assertThat(entry.hasMutableStatus).isFalse()
-        assertThat(entry.hasSliceSupport).isFalse()
     }
 
     @Test
@@ -137,7 +133,6 @@
                 .setIsSearchDataDynamic(false)
                 .setHasMutableStatus(true)
                 .setSearchDataFn { null }
-                .setSliceDataFn { _, _ -> null }
         val entry = entryBuilder.build()
         assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry.label).isEqualTo("myEntryDisplay")
@@ -146,7 +141,6 @@
         assertThat(entry.isAllowSearch).isTrue()
         assertThat(entry.isSearchDataDynamic).isFalse()
         assertThat(entry.hasMutableStatus).isTrue()
-        assertThat(entry.hasSliceSupport).isTrue()
 
         // Test disabled Spp
         val ownerDisabled = createSettingsPage("SppDisabled")
@@ -156,7 +150,6 @@
                 .setIsSearchDataDynamic(false)
                 .setHasMutableStatus(true)
                 .setSearchDataFn { null }
-                .setSliceDataFn { _, _ -> null }
         val entryDisabled = entryBuilderDisabled.build()
         assertThat(entryDisabled.id).isEqualTo(genEntryId("myEntry", ownerDisabled))
         assertThat(entryDisabled.label).isEqualTo("myEntryDisplay")
@@ -165,7 +158,6 @@
         assertThat(entryDisabled.isAllowSearch).isFalse()
         assertThat(entryDisabled.isSearchDataDynamic).isFalse()
         assertThat(entryDisabled.hasMutableStatus).isTrue()
-        assertThat(entryDisabled.hasSliceSupport).isFalse()
 
         // Clear search data fn
         val entry2 = entryBuilder.clearSearchDataFn().build()
@@ -181,7 +173,6 @@
         assertThat(entry3.isAllowSearch).isFalse()
         assertThat(entry3.isSearchDataDynamic).isFalse()
         assertThat(entry3.hasMutableStatus).isTrue()
-        assertThat(entry3.hasSliceSupport).isFalse()
     }
 
     @Test
@@ -200,7 +191,6 @@
         assertThat(entry.isAllowSearch).isTrue()
         assertThat(entry.isSearchDataDynamic).isFalse()
         assertThat(entry.hasMutableStatus).isFalse()
-        assertThat(entry.hasSliceSupport).isFalse()
         val searchData = entry.getSearchData(rtArguments)
         val statusData = entry.getStatusData(rtArguments)
         assertThat(searchData?.title).isEqualTo("myTitle")
@@ -208,25 +198,4 @@
         assertThat(statusData?.isDisabled).isTrue()
         assertThat(statusData?.isSwitchOff).isTrue()
     }
-
-    @Test
-    fun testSetSliceDataFn() {
-        SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = createSettingsPage("SppHome")
-        val entryId = genEntryId("myEntry", owner)
-        val emptySliceData = EntrySliceData()
-
-        val entryBuilder = SettingsEntryBuilder.create(owner, "myEntry").setSliceDataFn { uri, _ ->
-            return@setSliceDataFn if (uri.getEntryId() == entryId) emptySliceData else null
-        }
-        val entry = entryBuilder.build()
-        assertThat(entry.id).isEqualTo(entryId)
-        assertThat(entry.hasSliceSupport).isTrue()
-        assertThat(entry.getSliceData(Uri.EMPTY)).isNull()
-        assertThat(
-            entry.getSliceData(
-                Uri.Builder().scheme("content").appendSpaParams(entryId = entryId).build()
-            )
-        ).isEqualTo(emptySliceData)
-    }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
deleted file mode 100644
index b489afd..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2022 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.settingslib.spa.slice
-
-import android.net.Uri
-import androidx.core.os.bundleOf
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SliceUtilTest {
-    @Test
-    fun sliceUriTest() {
-        assertThat(Uri.EMPTY.getEntryId()).isNull()
-
-        // valid slice uri
-        val dest = "myRoute"
-        val entryId = "myEntry"
-        val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build()
-        assertThat(sliceUriWithoutParams.getEntryId()).isEqualTo(entryId)
-
-        val sliceUriWithParams =
-            Uri.Builder().appendSpaParams(dest, entryId, bundleOf("p1" to "v1")).build()
-        assertThat(sliceUriWithParams.getEntryId()).isEqualTo(entryId)
-    }
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
index 4f8fd79..9fcf886 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
@@ -17,9 +17,7 @@
 package com.android.settingslib.spa.tests.testutils
 
 import android.app.Activity
-import android.content.BroadcastReceiver
 import android.content.Context
-import android.content.Intent
 import android.os.Bundle
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -27,7 +25,6 @@
 import androidx.navigation.navArgument
 import com.android.settingslib.spa.framework.BrowseActivity
 import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.EntrySliceData
 import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.LogEvent
@@ -75,9 +72,6 @@
 }
 
 class BlankActivity : BrowseActivity()
-class BlankSliceBroadcastReceiver : BroadcastReceiver() {
-    override fun onReceive(p0: Context?, p1: Intent?) {}
-}
 
 object SppHome : SettingsPageProvider {
     override val name = "SppHome"
@@ -149,15 +143,7 @@
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val owner = this.createSettingsPage()
         return listOf(
-            SettingsEntryBuilder.create(owner, "Layer2Entry1")
-                .setSliceDataFn { _, _ ->
-                    return@setSliceDataFn object : EntrySliceData() {
-                        init {
-                            postValue(null)
-                        }
-                    }
-                }
-                .build(),
+            SettingsEntryBuilder.create(owner, "Layer2Entry1").build(),
             SettingsEntryBuilder.create(owner, "Layer2Entry2").build(),
         )
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index 4028b73..714f951 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -18,9 +18,7 @@
 
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
-import android.telephony.UiccCardInfo;
 import android.telephony.UiccPortInfo;
-import android.telephony.UiccSlotInfo;
 import android.telephony.UiccSlotMapping;
 
 public class DataServiceUtils {
@@ -71,53 +69,9 @@
         public static final String COLUMN_ID = "sudId";
 
         /**
-         * The name of the physical slot index column, see
-         * {@link UiccSlotMapping#getPhysicalSlotIndex()}.
-         */
-        public static final String COLUMN_PHYSICAL_SLOT_INDEX = "physicalSlotIndex";
-
-        /**
-         * The name of the logical slot index column, see
-         * {@link UiccSlotMapping#getLogicalSlotIndex()}.
-         */
-        public static final String COLUMN_LOGICAL_SLOT_INDEX = "logicalSlotIndex";
-
-        /**
-         * The name of the card ID column, see {@link UiccCardInfo#getCardId()}.
-         */
-        public static final String COLUMN_CARD_ID = "cardId";
-
-        /**
-         * The name of the eUICC state column, see {@link UiccCardInfo#isEuicc()}.
-         */
-        public static final String COLUMN_IS_EUICC = "isEuicc";
-
-        /**
-         * The name of the multiple enabled profiles supported state column, see
-         * {@link UiccCardInfo#isMultipleEnabledProfilesSupported()}.
-         */
-        public static final String COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED =
-                "isMultipleEnabledProfilesSupported";
-
-        /**
-         * The name of the card state column, see {@link UiccSlotInfo#getCardStateInfo()}.
-         */
-        public static final String COLUMN_CARD_STATE = "cardState";
-
-        /**
-         * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}.
-         */
-        public static final String COLUMN_IS_REMOVABLE = "isRemovable";
-
-        /**
          * The name of the active state column, see {@link UiccPortInfo#isActive()}.
          */
         public static final String COLUMN_IS_ACTIVE = "isActive";
-
-        /**
-         * The name of the port index column, see {@link UiccPortInfo#getPortIndex()}.
-         */
-        public static final String COLUMN_PORT_INDEX = "portIndex";
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
index c92204f..5f7fa27 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java
@@ -19,14 +19,13 @@
 import android.content.Context;
 import android.util.Log;
 
-import java.util.List;
-import java.util.Objects;
-
 import androidx.lifecycle.LiveData;
 import androidx.room.Database;
 import androidx.room.Room;
 import androidx.room.RoomDatabase;
-import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.List;
+import java.util.Objects;
 
 @Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class,
         MobileNetworkInfoEntity.class}, exportSchema = false, version = 1)
@@ -132,13 +131,6 @@
     }
 
     /**
-     * Query the UICC info by the subscription ID from the UiccInfoEntity table.
-     */
-    public LiveData<UiccInfoEntity> queryUiccInfoById(String id) {
-        return mUiccInfoDao().queryUiccInfoById(id);
-    }
-
-    /**
      * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity
      * table.
      */
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
index 7e60421..90e5189 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java
@@ -16,14 +16,14 @@
 
 package com.android.settingslib.mobile.dataservice;
 
-import java.util.List;
-
 import androidx.lifecycle.LiveData;
 import androidx.room.Dao;
 import androidx.room.Insert;
 import androidx.room.OnConflictStrategy;
 import androidx.room.Query;
 
+import java.util.List;
+
 @Dao
 public interface UiccInfoDao {
 
@@ -34,14 +34,6 @@
             + DataServiceUtils.UiccInfoData.COLUMN_ID)
     LiveData<List<UiccInfoEntity>> queryAllUiccInfos();
 
-    @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE "
-            + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :subId")
-    LiveData<UiccInfoEntity> queryUiccInfoById(String subId);
-
-    @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE "
-            + DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC + " = :isEuicc")
-    LiveData<List<UiccInfoEntity>> queryUiccInfosByEuicc(boolean isEuicc);
-
     @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME)
     int count();
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
index 2ccf295..0f80edf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java
@@ -26,20 +26,9 @@
 @Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME)
 public class UiccInfoEntity {
 
-    public UiccInfoEntity(@NonNull String subId, @NonNull String physicalSlotIndex,
-            int logicalSlotIndex, int cardId, boolean isEuicc,
-            boolean isMultipleEnabledProfilesSupported, int cardState, boolean isRemovable,
-            boolean isActive, int portIndex) {
+    public UiccInfoEntity(@NonNull String subId, boolean isActive) {
         this.subId = subId;
-        this.physicalSlotIndex = physicalSlotIndex;
-        this.logicalSlotIndex = logicalSlotIndex;
-        this.cardId = cardId;
-        this.isEuicc = isEuicc;
-        this.isMultipleEnabledProfilesSupported = isMultipleEnabledProfilesSupported;
-        this.cardState = cardState;
-        this.isRemovable = isRemovable;
         this.isActive = isActive;
-        this.portIndex = portIndex;
     }
 
     @PrimaryKey
@@ -47,48 +36,14 @@
     @NonNull
     public String subId;
 
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PHYSICAL_SLOT_INDEX)
-    @NonNull
-    public String physicalSlotIndex;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_LOGICAL_SLOT_INDEX)
-    public int logicalSlotIndex;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_ID)
-    public int cardId;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC)
-    public boolean isEuicc;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED)
-    public boolean isMultipleEnabledProfilesSupported;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_STATE)
-    public int cardState;
-
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_REMOVABLE)
-    public boolean isRemovable;
-
     @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE)
     public boolean isActive;
 
-    @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PORT_INDEX)
-    public int portIndex;
-
-
     @Override
     public int hashCode() {
         int result = 17;
         result = 31 * result + subId.hashCode();
-        result = 31 * result + physicalSlotIndex.hashCode();
-        result = 31 * result + logicalSlotIndex;
-        result = 31 * result + cardId;
-        result = 31 * result + Boolean.hashCode(isEuicc);
-        result = 31 * result + Boolean.hashCode(isMultipleEnabledProfilesSupported);
-        result = 31 * result + cardState;
-        result = 31 * result + Boolean.hashCode(isRemovable);
         result = 31 * result + Boolean.hashCode(isActive);
-        result = 31 * result + portIndex;
         return result;
     }
 
@@ -102,40 +57,15 @@
         }
 
         UiccInfoEntity info = (UiccInfoEntity) obj;
-        return  TextUtils.equals(subId, info.subId)
-                && TextUtils.equals(physicalSlotIndex, info.physicalSlotIndex)
-                && logicalSlotIndex == info.logicalSlotIndex
-                && cardId == info.cardId
-                && isEuicc == info.isEuicc
-                && isMultipleEnabledProfilesSupported == info.isMultipleEnabledProfilesSupported
-                && cardState == info.cardState
-                && isRemovable == info.isRemovable
-                && isActive == info.isActive
-                && portIndex == info.portIndex;
+        return TextUtils.equals(subId, info.subId) && isActive == info.isActive;
     }
 
     public String toString() {
         StringBuilder builder = new StringBuilder();
         builder.append(" {UiccInfoEntity(subId = ")
                 .append(subId)
-                .append(", logicalSlotIndex = ")
-                .append(physicalSlotIndex)
-                .append(", logicalSlotIndex = ")
-                .append(logicalSlotIndex)
-                .append(", cardId = ")
-                .append(cardId)
-                .append(", isEuicc = ")
-                .append(isEuicc)
-                .append(", isMultipleEnabledProfilesSupported = ")
-                .append(isMultipleEnabledProfilesSupported)
-                .append(", cardState = ")
-                .append(cardState)
-                .append(", isRemovable = ")
-                .append(isRemovable)
                 .append(", isActive = ")
                 .append(isActive)
-                .append(", portIndex = ")
-                .append(portIndex)
                 .append(")}");
         return builder.toString();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 7886e85..49b974f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -20,6 +20,7 @@
 import android.provider.Settings
 import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.settingslib.notification.modes.ZenMode
+import java.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -35,8 +36,7 @@
     override val globalZenMode: StateFlow<Int>
         get() = mutableZenMode.asStateFlow()
 
-    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
-        MutableStateFlow(listOf(TestModeBuilder.EXAMPLE))
+    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf())
     override val modes: Flow<List<ZenMode>>
         get() = mutableModesFlow.asStateFlow()
 
@@ -52,6 +52,10 @@
         mutableZenMode.value = zenMode
     }
 
+    fun addModes(zenModes: List<ZenMode>) {
+        mutableModesFlow.value += zenModes
+    }
+
     fun addMode(id: String, active: Boolean = false) {
         mutableModesFlow.value += newMode(id, active)
     }
@@ -60,6 +64,20 @@
         mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
     }
 
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        activateMode(zenMode.id)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        deactivateMode(zenMode.id)
+    }
+
+    fun activateMode(id: String) {
+        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
+        removeMode(id)
+        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build()
+    }
+
     fun deactivateMode(id: String) {
         val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
         removeMode(id)
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index b2fcb5f..0ff7f84 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -30,6 +30,7 @@
 import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
+import java.time.Duration
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
@@ -57,6 +58,10 @@
 
     /** A list of all existing priority modes. */
     val modes: Flow<List<ZenMode>>
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null)
+
+    fun deactivateMode(zenMode: ZenMode)
 }
 
 @SuppressLint("SharedFlowCreation")
@@ -178,4 +183,12 @@
             flowOf(emptyList())
         }
     }
+
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        backend.activateMode(zenMode, duration)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        backend.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 7b994d5..2f7cdd6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -37,6 +37,13 @@
     private ZenModeConfig.ZenRule mConfigZenRule;
 
     public static final ZenMode EXAMPLE = new TestModeBuilder().build();
+    public static final ZenMode MANUAL_DND = ZenMode.manualDndMode(
+            new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd"))
+                    .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+                    .build(),
+            true /* isActive */
+    );
 
     public TestModeBuilder() {
         // Reasonable defaults
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java
index 1e75014..3906749 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java
@@ -24,9 +24,9 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -109,7 +109,7 @@
             final boolean systemIme,
             final String name) {
         return new InputMethodPreference(
-                InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                 createInputMethodInfo(systemIme, name),
                 title,
                 true /* isAllowedByOrganization */,
@@ -119,7 +119,8 @@
 
     private static InputMethodInfo createInputMethodInfo(
             final boolean systemIme, final String name) {
-        final Context targetContext = InstrumentationRegistry.getTargetContext();
+        final Context targetContext =
+                InstrumentationRegistry.getInstrumentation().getTargetContext();
         final Locale systemLocale = targetContext
                 .getResources()
                 .getConfiguration()
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java
index f1c0bea..2c3478d 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java
@@ -18,9 +18,9 @@
 
 import android.text.TextUtils;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -104,7 +104,7 @@
         final Locale subtypeLocale = TextUtils.isEmpty(subtypeLanguageTag)
                 ? null : Locale.forLanguageTag(subtypeLanguageTag);
         return new InputMethodSubtypePreference(
-                InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                 key,
                 subtypeName,
                 subtypeLocale,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index a17076b..4e01a71 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -539,6 +539,7 @@
         "androidx.preference_preference",
         "androidx.appcompat_appcompat",
         "androidx.concurrent_concurrent-futures",
+        "androidx.concurrent_concurrent-futures-ktx",
         "androidx.mediarouter_mediarouter",
         "androidx.palette_palette",
         "androidx.legacy_legacy-preference-v14",
diff --git a/packages/SystemUI/aconfig/communal.aconfig b/packages/SystemUI/aconfig/communal.aconfig
index afcd8a9..f2b5efa 100644
--- a/packages/SystemUI/aconfig/communal.aconfig
+++ b/packages/SystemUI/aconfig/communal.aconfig
@@ -8,12 +8,3 @@
     bug: "304584416"
 }
 
-flag {
-    name: "enable_widget_picker_size_filter"
-    namespace: "communal"
-    description: "Enables passing a size filter to the widget picker"
-    bug: "345482907"
-   metadata {
-        purpose: PURPOSE_BUGFIX
-   }
-}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8e2f7c1..02e0423 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -870,6 +870,13 @@
 }
 
 flag {
+    name: "qs_ui_refactor_compose_fragment"
+    namespace: "systemui"
+    description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag."
+    bug: "325099249"
+}
+
+flag {
   name: "remove_dream_overlay_hide_on_touch"
   namespace: "systemui"
   description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions"
@@ -981,6 +988,16 @@
 }
 
 flag {
+  name: "communal_scene_ktf_refactor"
+  namespace: "systemui"
+  description: "refactors the syncing mechanism between communal STL and KTF state."
+  bug: "327225415"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "app_clips_backlinks"
   namespace: "systemui"
   description: "Enables Backlinks improvement feature in App Clips"
@@ -1189,6 +1206,16 @@
 }
 
 flag {
+   namespace: "systemui"
+   name: "remove_update_listener_in_qs_icon_view_impl"
+   description: "Remove update listeners in QsIconViewImpl class to avoid memory leak."
+   bug: "327078684"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
    name: "sim_pin_race_condition_on_restart"
    namespace: "systemui"
    description: "The SIM PIN screen may be shown incorrectly on reboot"
@@ -1196,4 +1223,24 @@
    metadata {
         purpose: PURPOSE_BUGFIX
    }
+}
+
+flag {
+   name: "sim_pin_talkback_fix_for_double_submit"
+   namespace: "systemui"
+   description: "The SIM PIN entry screens show the wrong message due"
+   bug: "346932439"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
+   name: "lockscreen_preview_renderer_create_on_main_thread"
+   namespace: "systemui"
+   description: "Force preview renderer to be created on the main thread"
+   bug: "343732179"
+   metadata {
+        purpose: PURPOSE_BUGFIX
+   }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 92f03d7..35db9e0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -209,7 +209,6 @@
                 backgroundType = backgroundType,
                 colors = colors,
                 content = content,
-                modifier = Modifier.horizontalNestedScrollToScene(),
             )
         }
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 768e653..1c02d3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -969,6 +969,8 @@
     val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget)
     val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget)
     val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
+    val unselectWidgetActionLabel =
+        stringResource(R.string.accessibility_action_label_unselect_widget)
     val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
     val selectedIndex =
         selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1009,18 +1011,7 @@
                                 contentListState.onSaveList()
                                 true
                             }
-                        val selectWidgetAction =
-                            CustomAccessibilityAction(clickActionLabel) {
-                                val currentWidgetKey =
-                                    index?.let {
-                                        keyAtIndexIfEditable(contentListState.list, index)
-                                    }
-                                viewModel.setSelectedKey(currentWidgetKey)
-                                true
-                            }
-
-                        val actions = mutableListOf(selectWidgetAction, deleteAction)
-
+                        val actions = mutableListOf(deleteAction)
                         if (selectedIndex != null && selectedIndex != index) {
                             actions.add(
                                 CustomAccessibilityAction(placeWidgetActionLabel) {
@@ -1032,6 +1023,21 @@
                             )
                         }
 
+                        if (!selected) {
+                            actions.add(
+                                CustomAccessibilityAction(clickActionLabel) {
+                                    viewModel.setSelectedKey(model.key)
+                                    true
+                                }
+                            )
+                        } else {
+                            actions.add(
+                                CustomAccessibilityAction(unselectWidgetActionLabel) {
+                                    viewModel.setSelectedKey(null)
+                                    true
+                                }
+                            )
+                        }
                         customActions = actions
                     }
                 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
index c34fb38..c993855 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt
@@ -55,7 +55,7 @@
             modifier
                 .pointerInput(isEnabled) {
                     if (isEnabled) {
-                        detectLongPressGesture { viewModel.onLongPress() }
+                        detectLongPressGesture { viewModel.onLongPress(isA11yAction = false) }
                     }
                 }
                 .pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index d629eec..f8bd633 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.animation.MeasurementInput
 
-private object MediaCarousel {
+object MediaCarousel {
     object Elements {
         internal val Content =
             ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
index 0398133..a22bc34 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
@@ -25,7 +25,7 @@
 /** [ElementScenePicker] implementation for the media carousel object. */
 object MediaScenePicker : ElementScenePicker {
 
-    private val shadeLockscreenFraction = 0.65f
+    const val SHADE_FRACTION = 0.66f
     private val scenes =
         setOf(
             Scenes.Lockscreen,
@@ -44,7 +44,7 @@
         return when {
             // TODO: 352052894 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
-                if (transition.progress < shadeLockscreenFraction) {
+                if (transition.progress < SHADE_FRACTION) {
                     Scenes.Lockscreen
                 } else {
                     Scenes.Shade
@@ -53,7 +53,7 @@
 
             // TODO: 345467290 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
-                if (transition.progress < 1f - shadeLockscreenFraction) {
+                if (transition.progress < 1f - SHADE_FRACTION) {
                     Scenes.Shade
                 } else {
                     Scenes.Lockscreen
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 776e166..c4970c5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -145,6 +145,7 @@
                     }
                     // Note: boundsInWindow doesn't scroll off the screen
                     stackScrollView.setHeadsUpTop(boundsInWindow.top)
+                    stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
                 }
     )
 }
@@ -471,7 +472,11 @@
             )
         }
         if (shouldIncludeHeadsUpSpace) {
-            HeadsUpNotificationSpace(stackScrollView = stackScrollView, viewModel = viewModel)
+            HeadsUpNotificationSpace(
+                stackScrollView = stackScrollView,
+                viewModel = viewModel,
+                modifier = Modifier.padding(top = topPadding)
+            )
         }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 2d5d259..3cf8e70 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -305,8 +305,7 @@
                     if (isCustomizerShowing) {
                         Modifier.fillMaxHeight().align(Alignment.TopCenter)
                     } else {
-                        Modifier.verticalNestedScrollToScene()
-                            .verticalScroll(
+                        Modifier.verticalScroll(
                                 scrollState,
                                 enabled = isScrollable,
                             )
@@ -408,7 +407,7 @@
         HeadsUpNotificationSpace(
             stackScrollView = notificationStackScrollView,
             viewModel = notificationsPlaceholderViewModel,
-            modifier = Modifier.align(Alignment.BottomCenter),
+            modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
             isPeekFromBottom = true,
         )
         NotificationScrollingStack(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index df47cba..7d46c75 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -25,6 +25,8 @@
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
 import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.MediaScenePicker
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Scenes
@@ -59,10 +61,13 @@
         fade(QuickSettings.Elements.SplitShadeQuickSettings)
         fade(QuickSettings.Elements.FooterActions)
     }
-    translate(
-        QuickSettings.Elements.QuickQuickSettings,
-        y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
-    )
+
+    val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION
+    val qsExpansionDiff =
+        ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight
+
+    translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation)
+    translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation))
     translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 805351e..ece8b40 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -514,8 +514,7 @@
                                     .sysuiResTag("expanded_qs_scroll_view")
                                     .weight(1f)
                                     .thenIf(!isCustomizerShowing) {
-                                        Modifier.verticalNestedScrollToScene()
-                                            .verticalScroll(
+                                        Modifier.verticalScroll(
                                                 quickSettingsScrollState,
                                                 enabled = isScrollable
                                             )
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index ea740a8..82c85d1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -30,7 +30,7 @@
  * the currently running transition, if there is one.
  */
 internal fun CoroutineScope.animateToScene(
-    layoutState: BaseSceneTransitionLayoutState,
+    layoutState: MutableSceneTransitionLayoutStateImpl,
     target: SceneKey,
     transitionKey: TransitionKey?,
 ): TransitionState.Transition? {
@@ -154,7 +154,7 @@
 }
 
 private fun CoroutineScope.animate(
-    layoutState: BaseSceneTransitionLayoutState,
+    layoutState: MutableSceneTransitionLayoutStateImpl,
     targetScene: SceneKey,
     transitionKey: TransitionKey?,
     isInitiatedByUserInput: Boolean,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 20b1303..665be53 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -64,6 +64,7 @@
     internal val orientation: Orientation,
     internal val coroutineScope: CoroutineScope,
 ) : DraggableHandler {
+    internal val nestedScrollKey = Any()
     /** The [DraggableHandler] can only have one active [DragController] at a time. */
     private var dragController: DragControllerImpl? = null
 
@@ -369,9 +370,6 @@
             // immediately go back B => A.
             if (targetScene != swipeTransition._currentScene) {
                 swipeTransition._currentScene = targetScene
-                with(draggableHandler.layoutImpl.state) {
-                    draggableHandler.coroutineScope.onChangeScene(targetScene.key)
-                }
             }
 
             swipeTransition.animateOffset(
@@ -511,7 +509,7 @@
 }
 
 private fun SwipeTransition(
-    layoutState: BaseSceneTransitionLayoutState,
+    layoutState: MutableSceneTransitionLayoutStateImpl,
     coroutineScope: CoroutineScope,
     fromScene: Scene,
     result: UserActionResult,
@@ -566,7 +564,7 @@
 
 private class SwipeTransition(
     val layoutImpl: SceneTransitionLayoutImpl,
-    val layoutState: BaseSceneTransitionLayoutState,
+    val layoutState: MutableSceneTransitionLayoutStateImpl,
     val coroutineScope: CoroutineScope,
     override val key: TransitionKey?,
     val _fromScene: Scene,
@@ -912,9 +910,9 @@
 internal class NestedScrollHandlerImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val orientation: Orientation,
-    private val topOrLeftBehavior: NestedScrollBehavior,
-    private val bottomOrRightBehavior: NestedScrollBehavior,
-    private val isExternalOverscrollGesture: () -> Boolean,
+    internal var topOrLeftBehavior: NestedScrollBehavior,
+    internal var bottomOrRightBehavior: NestedScrollBehavior,
+    internal var isExternalOverscrollGesture: () -> Boolean,
     private val pointersInfoOwner: PointersInfoOwner,
 ) {
     private val layoutState = layoutImpl.state
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 615d393..2b78b5a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -41,7 +41,6 @@
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.ObserverModifierNode
 import androidx.compose.ui.node.PointerInputModifierNode
-import androidx.compose.ui.node.TraversableNode
 import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.node.observeReads
@@ -139,16 +138,12 @@
     DelegatingNode(),
     PointerInputModifierNode,
     CompositionLocalConsumerModifierNode,
-    TraversableNode,
-    PointersInfoOwner,
     ObserverModifierNode {
     private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
     private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
     private val velocityTracker = VelocityTracker()
     private var previousEnabled: Boolean = false
 
-    override val traverseKey: Any = TRAVERSE_KEY
-
     var enabled: () -> Boolean = enabled
         set(value) {
             // Reset the pointer input whenever enabled changed.
@@ -208,7 +203,7 @@
     private var startedPosition: Offset? = null
     private var pointersDown: Int = 0
 
-    override fun pointersInfo(): PointersInfo {
+    internal fun pointersInfo(): PointersInfo {
         return PointersInfo(
             startedPosition = startedPosition,
             // Note: We could have 0 pointers during fling or for other reasons.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index ddff2f7..945043d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -18,12 +18,13 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
-import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 
 /**
  * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
@@ -67,7 +68,11 @@
      * In addition, during scene transitions, scroll events are consumed by the
      * [SceneTransitionLayout] instead of the scrollable component.
      */
-    EdgeAlways(canStartOnPostFling = true),
+    EdgeAlways(canStartOnPostFling = true);
+
+    companion object {
+        val Default = EdgeNoPreview
+    }
 }
 
 internal fun Modifier.nestedScrollToScene(
@@ -122,37 +127,60 @@
 }
 
 private class NestedScrollToSceneNode(
-    layoutImpl: SceneTransitionLayoutImpl,
-    orientation: Orientation,
-    topOrLeftBehavior: NestedScrollBehavior,
-    bottomOrRightBehavior: NestedScrollBehavior,
-    isExternalOverscrollGesture: () -> Boolean,
+    private var layoutImpl: SceneTransitionLayoutImpl,
+    private var orientation: Orientation,
+    private var topOrLeftBehavior: NestedScrollBehavior,
+    private var bottomOrRightBehavior: NestedScrollBehavior,
+    private var isExternalOverscrollGesture: () -> Boolean,
 ) : DelegatingNode() {
-    lateinit var pointersInfoOwner: PointersInfoOwner
-    private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
-        scenePriorityNestedScrollConnection(
-            layoutImpl = layoutImpl,
-            orientation = orientation,
-            topOrLeftBehavior = topOrLeftBehavior,
-            bottomOrRightBehavior = bottomOrRightBehavior,
-            isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfoOwner = { pointersInfoOwner.pointersInfo() }
-        )
+    private var scrollBehaviorOwner: ScrollBehaviorOwner? = null
 
-    private var nestedScrollNode: DelegatableNode =
-        nestedScrollModifierNode(
-            connection = priorityNestedScrollConnection,
-            dispatcher = null,
-        )
+    private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner {
+        var behaviorOwner = scrollBehaviorOwner
+        if (behaviorOwner == null) {
+            behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation))
+            scrollBehaviorOwner = behaviorOwner
+        }
+        return behaviorOwner
+    }
 
-    override fun onAttach() {
-        pointersInfoOwner = requireAncestorPointersInfoOwner()
-        delegate(nestedScrollNode)
+    private val updateScrollBehaviorsConnection =
+        object : NestedScrollConnection {
+            /**
+             * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior
+             * before our parent components. This gives them the option to override our behavior if
+             * they choose.
+             *
+             * The behavior can be communicated at every scroll gesture to ensure that the hierarchy
+             * is respected, even if one of our descendant nodes changes behavior after we set it.
+             */
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource,
+            ): Offset {
+                // If we have some remaining scroll, that scroll can be used to initiate a
+                // transition between scenes. We can assume that the behavior is only needed if
+                // there is some remaining amount.
+                if (available != Offset.Zero) {
+                    requireScrollBehaviorOwner()
+                        .updateScrollBehaviors(
+                            topOrLeftBehavior = topOrLeftBehavior,
+                            bottomOrRightBehavior = bottomOrRightBehavior,
+                            isExternalOverscrollGesture = isExternalOverscrollGesture,
+                        )
+                }
+
+                return Offset.Zero
+            }
+        }
+
+    init {
+        delegate(nestedScrollModifierNode(updateScrollBehaviorsConnection, dispatcher = null))
     }
 
     override fun onDetach() {
-        // Make sure we reset the scroll connection when this modifier is removed from composition
-        priorityNestedScrollConnection.reset()
+        scrollBehaviorOwner = null
     }
 
     fun update(
@@ -162,43 +190,10 @@
         bottomOrRightBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
     ) {
-        // Clean up the old nested scroll connection
-        priorityNestedScrollConnection.reset()
-        undelegate(nestedScrollNode)
-
-        // Create a new nested scroll connection
-        priorityNestedScrollConnection =
-            scenePriorityNestedScrollConnection(
-                layoutImpl = layoutImpl,
-                orientation = orientation,
-                topOrLeftBehavior = topOrLeftBehavior,
-                bottomOrRightBehavior = bottomOrRightBehavior,
-                isExternalOverscrollGesture = isExternalOverscrollGesture,
-                pointersInfoOwner = pointersInfoOwner,
-            )
-        nestedScrollNode =
-            nestedScrollModifierNode(
-                connection = priorityNestedScrollConnection,
-                dispatcher = null,
-            )
-        delegate(nestedScrollNode)
+        this.layoutImpl = layoutImpl
+        this.orientation = orientation
+        this.topOrLeftBehavior = topOrLeftBehavior
+        this.bottomOrRightBehavior = bottomOrRightBehavior
+        this.isExternalOverscrollGesture = isExternalOverscrollGesture
     }
 }
-
-private fun scenePriorityNestedScrollConnection(
-    layoutImpl: SceneTransitionLayoutImpl,
-    orientation: Orientation,
-    topOrLeftBehavior: NestedScrollBehavior,
-    bottomOrRightBehavior: NestedScrollBehavior,
-    isExternalOverscrollGesture: () -> Boolean,
-    pointersInfoOwner: PointersInfoOwner,
-) =
-    NestedScrollHandlerImpl(
-            layoutImpl = layoutImpl,
-            orientation = orientation,
-            topOrLeftBehavior = topOrLeftBehavior,
-            bottomOrRightBehavior = bottomOrRightBehavior,
-            isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfoOwner = pointersInfoOwner,
-        )
-        .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index 734241e..d3e2a1c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -35,7 +35,7 @@
 
 @Composable
 internal fun PredictiveBackHandler(
-    state: BaseSceneTransitionLayoutState,
+    state: MutableSceneTransitionLayoutStateImpl,
     coroutineScope: CoroutineScope,
     targetSceneForBack: SceneKey? = null,
 ) {
@@ -65,7 +65,7 @@
 }
 
 private class PredictiveBackTransition(
-    val state: BaseSceneTransitionLayoutState,
+    val state: MutableSceneTransitionLayoutStateImpl,
     val coroutineScope: CoroutineScope,
     fromScene: SceneKey,
     toScene: SceneKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 0c467b1..2fc4526 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -207,8 +207,8 @@
      * @param rightBehavior when we should perform the overscroll animation at the right.
      */
     fun Modifier.horizontalNestedScrollToScene(
-        leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
-        rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        leftBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+        rightBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
         isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
@@ -220,8 +220,8 @@
      * @param bottomBehavior when we should perform the overscroll animation at the bottom.
      */
     fun Modifier.verticalNestedScrollToScene(
-        topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
-        bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        topBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+        bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
         isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
@@ -514,7 +514,7 @@
     val coroutineScope = rememberCoroutineScope()
     val layoutImpl = remember {
         SceneTransitionLayoutImpl(
-                state = state as BaseSceneTransitionLayoutState,
+                state = state as MutableSceneTransitionLayoutStateImpl,
                 density = density,
                 layoutDirection = layoutDirection,
                 swipeSourceDetector = swipeSourceDetector,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 3e48c42..32db0b7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -44,7 +44,7 @@
 
 @Stable
 internal class SceneTransitionLayoutImpl(
-    internal val state: BaseSceneTransitionLayoutState,
+    internal val state: MutableSceneTransitionLayoutStateImpl,
     internal var density: Density,
     internal var layoutDirection: LayoutDirection,
     internal var swipeSourceDetector: SwipeSourceDetector,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 56c8752..48bc251 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -200,7 +200,7 @@
          * transition.
          *
          * Important: These will be set exactly once, when this transition is
-         * [started][BaseSceneTransitionLayoutState.startTransition].
+         * [started][MutableSceneTransitionLayoutStateImpl.startTransition].
          */
         internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
         private var fromOverscrollSpec: OverscrollSpecImpl? = null
@@ -332,13 +332,16 @@
     }
 }
 
-internal abstract class BaseSceneTransitionLayoutState(
+/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
+internal class MutableSceneTransitionLayoutStateImpl(
     initialScene: SceneKey,
-    protected var stateLinks: List<StateLink>,
+    override var transitions: SceneTransitions = transitions {},
+    internal val canChangeScene: (SceneKey) -> Boolean = { true },
+    private val stateLinks: List<StateLink> = emptyList(),
 
     // TODO(b/290930950): Remove this flag.
-    internal var enableInterruptions: Boolean,
-) : SceneTransitionLayoutState {
+    internal val enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
+) : MutableSceneTransitionLayoutState {
     private val creationThread: Thread = Thread.currentThread()
 
     /**
@@ -374,17 +377,6 @@
     @VisibleForTesting
     internal val finishedTransitions = mutableMapOf<TransitionState.Transition, SceneKey>()
 
-    /** Whether we can transition to the given [scene]. */
-    internal abstract fun canChangeScene(scene: SceneKey): Boolean
-
-    /**
-     * Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
-     *
-     * When this is called, the source of truth for the current scene should be changed so that
-     * [transitionState] will animate and settle to [scene].
-     */
-    internal abstract fun CoroutineScope.onChangeScene(scene: SceneKey)
-
     internal fun checkThread() {
         val current = Thread.currentThread()
         if (current !== creationThread) {
@@ -409,6 +401,20 @@
         return transition.isTransitioningBetween(scene, other)
     }
 
+    override fun setTargetScene(
+        targetScene: SceneKey,
+        coroutineScope: CoroutineScope,
+        transitionKey: TransitionKey?,
+    ): TransitionState.Transition? {
+        checkThread()
+
+        return coroutineScope.animateToScene(
+            layoutState = this@MutableSceneTransitionLayoutStateImpl,
+            target = targetScene,
+            transitionKey = transitionKey,
+        )
+    }
+
     /**
      * Start a new [transition].
      *
@@ -600,7 +606,7 @@
         }
     }
 
-    fun snapToScene(scene: SceneKey) {
+    override fun snapToScene(scene: SceneKey) {
         checkThread()
 
         // Force finish all transitions.
@@ -674,37 +680,6 @@
     }
 }
 
-/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
-internal class MutableSceneTransitionLayoutStateImpl(
-    initialScene: SceneKey,
-    override var transitions: SceneTransitions = transitions {},
-    private val canChangeScene: (SceneKey) -> Boolean = { true },
-    stateLinks: List<StateLink> = emptyList(),
-    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
-) :
-    MutableSceneTransitionLayoutState,
-    BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
-    override fun setTargetScene(
-        targetScene: SceneKey,
-        coroutineScope: CoroutineScope,
-        transitionKey: TransitionKey?,
-    ): TransitionState.Transition? {
-        checkThread()
-
-        return coroutineScope.animateToScene(
-            layoutState = this@MutableSceneTransitionLayoutStateImpl,
-            target = targetScene,
-            transitionKey = transitionKey,
-        )
-    }
-
-    override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)
-
-    override fun CoroutineScope.onChangeScene(scene: SceneKey) {
-        setTargetScene(scene, coroutineScope = this)
-    }
-}
-
 private const val TAG = "SceneTransitionLayoutState"
 
 /** Whether support for interruptions in enabled by default. */
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index aeb6262..b8010f2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -20,11 +20,15 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.unit.IntSize
 
 /**
@@ -53,7 +57,7 @@
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
 ) : DelegatingNode(), PointerInputModifierNode {
-    private val delegate =
+    private val multiPointerDraggableNode =
         delegate(
             MultiPointerDraggableNode(
                 orientation = draggableHandler.orientation,
@@ -74,21 +78,41 @@
                 // Make sure to update the delegate orientation. Note that this will automatically
                 // reset the underlying pointer input handler, so previous gestures will be
                 // cancelled.
-                delegate.orientation = value.orientation
+                multiPointerDraggableNode.orientation = value.orientation
             }
         }
 
+    private val nestedScrollHandlerImpl =
+        NestedScrollHandlerImpl(
+            layoutImpl = draggableHandler.layoutImpl,
+            orientation = draggableHandler.orientation,
+            topOrLeftBehavior = NestedScrollBehavior.Default,
+            bottomOrRightBehavior = NestedScrollBehavior.Default,
+            isExternalOverscrollGesture = { false },
+            pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() },
+        )
+
+    init {
+        delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null))
+        delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl))
+    }
+
+    override fun onDetach() {
+        // Make sure we reset the scroll connection when this modifier is removed from composition
+        nestedScrollHandlerImpl.connection.reset()
+    }
+
     override fun onPointerEvent(
         pointerEvent: PointerEvent,
         pass: PointerEventPass,
         bounds: IntSize,
-    ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
+    ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds)
 
-    override fun onCancelPointerInput() = delegate.onCancelPointerInput()
+    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
 
     private fun enabled(): Boolean {
         return draggableHandler.isDrivingTransition ||
-            currentScene().shouldEnableSwipes(delegate.orientation)
+            currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation)
     }
 
     private fun currentScene(): Scene {
@@ -118,3 +142,43 @@
         return currentScene().shouldEnableSwipes(oppositeOrientation)
     }
 }
+
+/** Find the [ScrollBehaviorOwner] for the current orientation. */
+internal fun DelegatableNode.requireScrollBehaviorOwner(
+    draggableHandler: DraggableHandlerImpl
+): ScrollBehaviorOwner {
+    val ancestorNode =
+        checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) {
+            "This should never happen! Couldn't find a ScrollBehaviorOwner. " +
+                "Are we inside an SceneTransitionLayout?"
+        }
+    return ancestorNode as ScrollBehaviorOwner
+}
+
+internal fun interface ScrollBehaviorOwner {
+    fun updateScrollBehaviors(
+        topOrLeftBehavior: NestedScrollBehavior,
+        bottomOrRightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean,
+    )
+}
+
+/**
+ * We need a node that receives the desired behavior.
+ *
+ * TODO(b/353234530) move this logic into [SwipeToSceneNode]
+ */
+private class ScrollBehaviorOwnerNode(
+    override val traverseKey: Any,
+    val nestedScrollHandlerImpl: NestedScrollHandlerImpl
+) : Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
+    override fun updateScrollBehaviors(
+        topOrLeftBehavior: NestedScrollBehavior,
+        bottomOrRightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean
+    ) {
+        nestedScrollHandlerImpl.topOrLeftBehavior = topOrLeftBehavior
+        nestedScrollHandlerImpl.bottomOrRightBehavior = bottomOrRightBehavior
+        nestedScrollHandlerImpl.isExternalOverscrollGesture = isExternalOverscrollGesture
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
index 6c29946..2018d6e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/StateLink.kt
@@ -16,7 +16,7 @@
 
 package com.android.compose.animation.scene.transition.link
 
-import com.android.compose.animation.scene.BaseSceneTransitionLayoutState
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneTransitionLayoutState
 import com.android.compose.animation.scene.TransitionKey
@@ -25,7 +25,7 @@
 /** A link between a source (implicit) and [target] `SceneTransitionLayoutState`. */
 class StateLink(target: SceneTransitionLayoutState, val transitionLinks: List<TransitionLink>) {
 
-    internal val target = target as BaseSceneTransitionLayoutState
+    internal val target = target as MutableSceneTransitionLayoutStateImpl
 
     /**
      * Links two transitions (source and target) together.
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 7988e0e..c91151e 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
@@ -797,8 +797,6 @@
                 scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) {
                     Box(
                         Modifier
-                            // Unconsumed scroll gesture will be intercepted by STL
-                            .verticalNestedScrollToScene()
                             // A scrollable that does not consume the scroll gesture
                             .scrollable(
                                 rememberScrollableState(consumeScrollDelta = { 0f }),
@@ -875,8 +873,6 @@
                 ) {
                     Box(
                         Modifier
-                            // Unconsumed scroll gesture will be intercepted by STL
-                            .verticalNestedScrollToScene()
                             // A scrollable that does not consume the scroll gesture
                             .scrollable(
                                 rememberScrollableState(consumeScrollDelta = { 0f }),
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
new file mode 100644
index 0000000..311a580
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation.Vertical
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.subjects.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NestedScrollToSceneTest {
+    @get:Rule val rule = createComposeRule()
+
+    private var touchSlop = 0f
+    private val layoutWidth: Dp = 200.dp
+    private val layoutHeight = 400.dp
+
+    private fun setup2ScenesAndScrollTouchSlop(
+        modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+    ): MutableSceneTransitionLayoutState {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(SceneA, transitions = EmptyTestTransitions)
+            }
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(
+                state = state,
+                modifier = Modifier.size(layoutWidth, layoutHeight)
+            ) {
+                scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) {
+                    Spacer(modifierSceneA().fillMaxSize())
+                }
+                scene(SceneB, userActions = mapOf(Swipe.Down to SceneA)) {
+                    Spacer(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        pointerDownAndScrollTouchSlop()
+
+        assertThat(state.transitionState).isIdle()
+
+        return state
+    }
+
+    private fun pointerDownAndScrollTouchSlop() {
+        rule.onRoot().performTouchInput {
+            val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
+            down(middleTop)
+            // Scroll touchSlop
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+    }
+
+    private fun scrollDown(percent: Float = 1f) {
+        rule.onRoot().performTouchInput {
+            moveBy(Offset(0f, layoutHeight.toPx() * percent), delayMillis = 1_000)
+        }
+    }
+
+    private fun scrollUp(percent: Float = 1f) = scrollDown(-percent)
+
+    private fun pointerUp() {
+        rule.onRoot().performTouchInput { up() }
+    }
+
+    @Test
+    fun scrollableElementsInSTL_shouldHavePriority() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // A scrollable that consumes the scroll gesture
+                .scrollable(rememberScrollableState { it }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+
+        // Consumed by the scrollable element
+        assertThat(state.transitionState).isIdle()
+    }
+
+    @Test
+    fun unconsumedScrollEvents_canBeConsumedBySTLByDefault() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // A scrollable that does not consume the scroll gesture
+                .scrollable(rememberScrollableState { 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        // STL will start a transition with the remaining scroll
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        scrollUp(percent = 1f)
+        assertThat(transition).hasProgress(1.5f)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_DuringTransitionBetweenScenes() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        pointerUp()
+        assertThat(state.transitionState).isIdle()
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeNoPreview() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.EdgeNoPreview
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        pointerUp()
+        assertThat(state.transitionState).isIdle()
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeWithPreview() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.EdgeWithPreview
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        val transition1 = assertThat(state.transitionState).isTransition()
+        assertThat(transition1).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        val transition2 = assertThat(state.transitionState).isTransition()
+        assertThat(transition2).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeAlways() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_multipleRequests() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // This verticalNestedScrollToScene is closer the STL (an ancestor node)
+                .verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+                // Another verticalNestedScrollToScene modifier
+                .verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+                )
+                .scrollable(rememberScrollableState { 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        // EdgeAlways always consume the remaining scroll, DuringTransitionBetweenScenes does not.
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+    }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 41bf630..52cceec 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -153,7 +153,7 @@
         sourceTo: SceneKey? = SceneB,
         targetFrom: SceneKey? = SceneC,
         targetTo: SceneKey = SceneD
-    ): Pair<BaseSceneTransitionLayoutState, BaseSceneTransitionLayoutState> {
+    ): Pair<MutableSceneTransitionLayoutStateImpl, MutableSceneTransitionLayoutStateImpl> {
         val parentState = MutableSceneTransitionLayoutState(parentInitialScene)
         val link =
             listOf(
@@ -164,8 +164,8 @@
             )
         val childState = MutableSceneTransitionLayoutState(childInitialScene, stateLinks = link)
         return Pair(
-            parentState as BaseSceneTransitionLayoutState,
-            childState as BaseSceneTransitionLayoutState
+            parentState as MutableSceneTransitionLayoutStateImpl,
+            childState as MutableSceneTransitionLayoutStateImpl
         )
     }
 
@@ -187,7 +187,7 @@
     @Test
     fun linkedTransition_transitiveLink() {
         val parentParentState =
-            MutableSceneTransitionLayoutState(SceneB) as BaseSceneTransitionLayoutState
+            MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl
         val parentLink =
             listOf(
                 StateLink(
@@ -197,7 +197,7 @@
             )
         val parentState =
             MutableSceneTransitionLayoutState(SceneC, stateLinks = parentLink)
-                as BaseSceneTransitionLayoutState
+                as MutableSceneTransitionLayoutStateImpl
         val link =
             listOf(
                 StateLink(
@@ -207,7 +207,7 @@
             )
         val childState =
             MutableSceneTransitionLayoutState(SceneA, stateLinks = link)
-                as BaseSceneTransitionLayoutState
+                as MutableSceneTransitionLayoutStateImpl
 
         val childTransition = transition(SceneA, SceneB)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 5cf4f16..7fd9ce2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -146,6 +146,14 @@
         verify(mShadeViewController, never()).handleExternalTouch(any())
     }
 
+    @Test
+    fun testCancelMotionEvent_popsTouchSession() {
+        swipe(Direction.DOWN)
+        val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
+        mInputListenerCaptor.lastValue.onInputEvent(event)
+        verify(mTouchSession).pop()
+    }
+
     /**
      * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
      * touch handler's gesture listener.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
index a8bdc7c..1f5e30c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt
@@ -88,24 +88,6 @@
         }
 
     @Test
-    fun isDisclaimerDismissed_byDefault_isFalse() =
-        testScope.runTest {
-            val isDisclaimerDismissed by
-                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
-    @Test
-    fun isDisclaimerDismissed_onSet_isTrue() =
-        testScope.runTest {
-            val isDisclaimerDismissed by
-                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))
-
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            assertThat(isDisclaimerDismissed).isTrue()
-        }
-
-    @Test
     fun getSharedPreferences_whenFileRestored() =
         testScope.runTest {
             val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 5cdbe9c..9539c04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -84,6 +84,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -1059,6 +1060,25 @@
         )
     }
 
+    @Test
+    fun dismissDisclaimerSetsDismissedFlag() =
+        testScope.runTest {
+            val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
+            assertThat(disclaimerDismissed).isFalse()
+            underTest.setDisclaimerDismissed()
+            assertThat(disclaimerDismissed).isTrue()
+        }
+
+    @Test
+    fun dismissDisclaimerTimeoutResetsDismissedFlag() =
+        testScope.runTest {
+            val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
+            underTest.setDisclaimerDismissed()
+            assertThat(disclaimerDismissed).isTrue()
+            advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS)
+            assertThat(disclaimerDismissed).isFalse()
+        }
+
     private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) {
         whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id)))
             .thenReturn(disabledFlags)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
index 7b79d28..9a92f76 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt
@@ -74,40 +74,6 @@
             assertThat(isCtaDismissed).isFalse()
         }
 
-    @Test
-    fun setDisclaimerDismissed_currentUser() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isFalse()
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            assertThat(isDisclaimerDismissed).isTrue()
-        }
-
-    @Test
-    fun setDisclaimerDismissed_anotherUser() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isFalse()
-            underTest.setDisclaimerDismissed(SECONDARY_USER)
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
-    @Test
-    fun isDisclaimerDismissed_userSwitch() =
-        testScope.runTest {
-            setSelectedUser(MAIN_USER)
-            underTest.setDisclaimerDismissed(MAIN_USER)
-            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)
-
-            assertThat(isDisclaimerDismissed).isTrue()
-            setSelectedUser(SECONDARY_USER)
-            assertThat(isDisclaimerDismissed).isFalse()
-        }
-
     private suspend fun setSelectedUser(user: UserInfo) {
         with(kosmos.fakeUserRepository) {
             setUserInfos(listOf(user))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
new file mode 100644
index 0000000..f7f70c1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -0,0 +1,685 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Idle
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+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)
+@EnableFlags(FLAG_COMMUNAL_HUB, FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+@DisableSceneContainer
+class CommunalSceneTransitionInteractorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply { keyguardTransitionRepository = realKeyguardTransitionRepository }
+    private val testScope = kosmos.testScope
+
+    private val underTest by lazy { kosmos.communalSceneTransitionInteractor }
+    private val keyguardTransitionRepository by lazy { kosmos.realKeyguardTransitionRepository }
+
+    private val ownerName = CommunalSceneTransitionInteractor::class.java.simpleName
+    private val progress = MutableSharedFlow<Float>()
+
+    private val sceneTransitions =
+        MutableStateFlow<ObservableTransitionState>(Idle(CommunalScenes.Blank))
+
+    private val blankToHub =
+        ObservableTransitionState.Transition(
+            fromScene = CommunalScenes.Blank,
+            toScene = CommunalScenes.Communal,
+            currentScene = flowOf(CommunalScenes.Blank),
+            progress = progress,
+            isInitiatedByUserInput = false,
+            isUserInputOngoing = flowOf(false),
+        )
+
+    private val hubToBlank =
+        ObservableTransitionState.Transition(
+            fromScene = CommunalScenes.Communal,
+            toScene = CommunalScenes.Blank,
+            currentScene = flowOf(CommunalScenes.Communal),
+            progress = progress,
+            isInitiatedByUserInput = false,
+            isUserInputOngoing = flowOf(false),
+        )
+
+    @Before
+    fun setup() {
+        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+        underTest.start()
+        kosmos.communalSceneRepository.setTransitionState(sceneTransitions)
+        testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) }
+    }
+
+    /** Transition from blank to glanceable hub. This is the default case. */
+    @Test
+    fun transition_from_blank_end_in_hub() =
+        testScope.runTest {
+            sceneTransitions.value = blankToHub
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(1f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 1f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            sceneTransitions.value = Idle(CommunalScenes.Communal)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = FINISHED,
+                        value = 1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    /** Transition from hub to lockscreen. */
+    @Test
+    fun transition_from_hub_end_in_lockscreen() =
+        testScope.runTest {
+            sceneTransitions.value = hubToBlank
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            sceneTransitions.value = Idle(CommunalScenes.Blank)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = FINISHED,
+                        value = 1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    /** Transition from hub to dream. */
+    @Test
+    fun transition_from_hub_end_in_dream() =
+        testScope.runTest {
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            runCurrent()
+
+            sceneTransitions.value = hubToBlank
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = DREAMING,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = DREAMING,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            sceneTransitions.value = Idle(CommunalScenes.Blank)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = DREAMING,
+                        transitionState = FINISHED,
+                        value = 1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    /** Transition from blank to hub, then settle back in blank. */
+    @Test
+    fun transition_from_blank_end_in_blank() =
+        testScope.runTest {
+            sceneTransitions.value = blankToHub
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+            val allSteps by collectValues(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            val numToDrop = allSteps.size
+            // Settle back in blank
+            sceneTransitions.value = Idle(CommunalScenes.Blank)
+
+            // Assert that KTF reversed transition back to lockscreen.
+            assertThat(allSteps.drop(numToDrop))
+                .containsExactly(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = CANCELED,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    ),
+                    // Transition back to lockscreen
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = STARTED,
+                        value = 0.6f,
+                        ownerName = ownerName,
+                    ),
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = FINISHED,
+                        value = 1f,
+                        ownerName = ownerName,
+                    ),
+                )
+                .inOrder()
+        }
+
+    @Test
+    fun transition_to_occluded_with_changed_scene_respected_just_once() =
+        testScope.runTest {
+            underTest.onSceneAboutToChange(CommunalScenes.Blank, OCCLUDED)
+            runCurrent()
+            sceneTransitions.value = hubToBlank
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            sceneTransitions.value = blankToHub
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = OCCLUDED,
+                        to = GLANCEABLE_HUB,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            sceneTransitions.value = hubToBlank
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    @Test
+    fun transition_from_blank_interrupted() =
+        testScope.runTest {
+            sceneTransitions.value = blankToHub
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+            val allSteps by collectValues(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            val numToDrop = allSteps.size
+            // Transition back from hub to blank, interrupting
+            // the current transition.
+            sceneTransitions.value = hubToBlank
+
+            assertThat(allSteps.drop(numToDrop))
+                .containsExactly(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        value = 1f,
+                        transitionState = FINISHED,
+                        ownerName = ownerName,
+                    ),
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        value = 0f,
+                        transitionState = STARTED,
+                        ownerName = ownerName,
+                    ),
+                )
+                .inOrder()
+
+            progress.emit(0.1f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = RUNNING,
+                        value = 0.1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    /**
+     * Blank -> Hub transition interrupted by a new Blank -> Hub transition. KTF state should not be
+     * updated in this case.
+     */
+    @Test
+    fun transition_to_hub_duplicate_does_not_change_ktf() =
+        testScope.runTest {
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    fromScene = CommunalScenes.Blank,
+                    toScene = CommunalScenes.Communal,
+                    currentScene = flowOf(CommunalScenes.Blank),
+                    progress = progress,
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+            val allSteps by collectValues(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            val sizeBefore = allSteps.size
+            val newProgress = MutableSharedFlow<Float>()
+            sceneTransitions.value =
+                ObservableTransitionState.Transition(
+                    fromScene = CommunalScenes.Blank,
+                    toScene = CommunalScenes.Communal,
+                    currentScene = flowOf(CommunalScenes.Blank),
+                    progress = newProgress,
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                )
+
+            // No new KTF steps emitted as a result of the new transition.
+            assertThat(allSteps).hasSize(sizeBefore)
+
+            // Progress is now tracked by the new flow.
+            newProgress.emit(0.1f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = GLANCEABLE_HUB,
+                        transitionState = RUNNING,
+                        value = 0.1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+
+    /**
+     * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL still finishes in Blank.
+     * After a KTF transition is started (GLANCEABLE_HUB -> LOCKSCREEN) KTF immediately considers
+     * the active scene to be LOCKSCREEN. This means that all listeners for LOCKSCREEN are active
+     * and may start a new transition LOCKSCREEN -> *. Here we test LOCKSCREEN -> OCCLUDED.
+     *
+     * KTF is allowed to already start and play the other transition, while the STL transition may
+     * finish later (gesture completes much later). When we eventually settle the STL transition in
+     * Blank we do not want to force KTF back to its original destination (LOCKSCREEN). Instead, for
+     * this scenario the settle can be ignored.
+     */
+    @Test
+    fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_blank() =
+        testScope.runTest {
+            sceneTransitions.value = hubToBlank
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            // Start another transition externally while our scene
+            // transition is happening.
+            keyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = "external",
+                    from = LOCKSCREEN,
+                    to = OCCLUDED,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = "external",
+                    )
+                )
+
+            // Scene progress should not affect KTF transition anymore
+            progress.emit(0.7f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = "external",
+                    )
+                )
+
+            // Scene transition still finishes but should not impact KTF transition
+            sceneTransitions.value = Idle(CommunalScenes.Blank)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = "external",
+                    )
+                )
+        }
+
+    /**
+     * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL finishes back in Hub.
+     *
+     * This is similar to the previous scenario but the gesture may have been interrupted by any
+     * other transition. KTF needs to immediately finish in GLANCEABLE_HUB (there is a jump cut).
+     */
+    @Test
+    fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_hub() =
+        testScope.runTest {
+            sceneTransitions.value = hubToBlank
+
+            val currentStep by collectLastValue(keyguardTransitionRepository.transitions)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            progress.emit(0.4f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = GLANCEABLE_HUB,
+                        to = LOCKSCREEN,
+                        transitionState = RUNNING,
+                        value = 0.4f,
+                        ownerName = ownerName,
+                    )
+                )
+
+            // Start another transition externally while our scene
+            // transition is happening.
+            keyguardTransitionRepository.startTransition(
+                TransitionInfo(
+                    ownerName = "external",
+                    from = LOCKSCREEN,
+                    to = OCCLUDED,
+                    animator = null,
+                    modeOnCanceled = TransitionModeOnCanceled.RESET
+                )
+            )
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = "external",
+                    )
+                )
+
+            // Scene progress should not affect KTF transition anymore
+            progress.emit(0.7f)
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = LOCKSCREEN,
+                        to = OCCLUDED,
+                        transitionState = STARTED,
+                        value = 0f,
+                        ownerName = "external",
+                    )
+                )
+
+            // We land back in communal.
+            sceneTransitions.value = Idle(CommunalScenes.Communal)
+
+            assertThat(currentStep)
+                .isEqualTo(
+                    TransitionStep(
+                        from = OCCLUDED,
+                        to = GLANCEABLE_HUB,
+                        transitionState = FINISHED,
+                        value = 1f,
+                        ownerName = ownerName,
+                    )
+                )
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index b138fb3..f8906ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -65,6 +65,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -352,6 +353,21 @@
         }
 
     @Test
+    fun showDisclaimer_trueWhenTimeout() =
+        testScope.runTest {
+            underTest.setEditModeState(EditModeState.SHOWING)
+            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+
+            val showDisclaimer by collectLastValue(underTest.showDisclaimer)
+
+            assertThat(showDisclaimer).isTrue()
+            underTest.onDisclaimerDismissed()
+            assertThat(showDisclaimer).isFalse()
+            advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS)
+            assertThat(showDisclaimer).isTrue()
+        }
+
+    @Test
     fun scrollPosition_persistedOnEditCleanup() {
         val index = 2
         val offset = 30
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt
index ac50db4..e36fd75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorControllerTest.kt
@@ -14,17 +14,20 @@
  * limitations under the License.
  */
 
+package com.android.systemui.communal.widgets
+
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -34,6 +37,7 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalTransitionAnimatorControllerTest : SysuiTestCase() {
@@ -66,7 +70,7 @@
     }
 
     @Test
-    fun animationCancelled_launchingWidgetStateIsClearedAndSceneIsNotChanged() {
+    fun animationCancelled_launchingWidgetStateIsCleared() {
         with(kosmos) {
             testScope.runTest {
                 val launching by collectLastValue(communalSceneInteractor.isLaunchingWidget)
@@ -81,9 +85,12 @@
                 assertTrue(launching!!)
                 verify(controller).onIntentStarted(willAnimate = true)
 
+                underTest.onTransitionAnimationStart(isExpandingFullyAbove = true)
+                assertTrue(launching!!)
+                verify(controller).onTransitionAnimationStart(isExpandingFullyAbove = true)
+
                 underTest.onTransitionAnimationCancelled(newKeyguardOccludedState = true)
                 assertFalse(launching!!)
-                Truth.assertThat(scene).isEqualTo(CommunalScenes.Communal)
                 verify(controller).onTransitionAnimationCancelled(newKeyguardOccludedState = true)
             }
         }
@@ -105,6 +112,12 @@
                 assertTrue(launching!!)
                 verify(controller).onIntentStarted(willAnimate = true)
 
+                underTest.onTransitionAnimationStart(isExpandingFullyAbove = true)
+                assertTrue(launching!!)
+                verify(controller).onTransitionAnimationStart(isExpandingFullyAbove = true)
+
+                testScope.advanceTimeBy(ActivityTransitionAnimator.TIMINGS.totalDuration)
+
                 underTest.onTransitionAnimationEnd(isExpandingFullyAbove = true)
                 assertFalse(launching!!)
                 Truth.assertThat(scene).isEqualTo(CommunalScenes.Blank)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 73ef775..88ba041 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -84,7 +84,7 @@
         verify(mockAnimator, atLeastOnce()).addListener(captor.capture())
 
         captor.allValues.forEach { it.onAnimationEnd(mockAnimator) }
-        verify(stateController).setExitAnimationsRunning(false)
+        verify(stateController, times(2)).setExitAnimationsRunning(false)
     }
 
     @Test
@@ -154,4 +154,10 @@
             }
         )
     }
+
+    @Test
+    fun testCancelAnimations_clearsExitAnimationsRunning() {
+        controller.cancelAnimations()
+        verify(stateController).setExitAnimationsRunning(false)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
new file mode 100644
index 0000000..ee51e37
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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 com.android.systemui.education.domain.interactor
+
+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.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.education.GestureType.BACK_GESTURE
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.keyboardTouchpadEduStatsInteractor
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCount() =
+        testScope.runTest {
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+                )
+            val originalValue = model!!.signalCount
+            underTest.incrementSignalCount(BACK_GESTURE)
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataAddedOnUpdateShortcutTriggerTime() =
+        testScope.runTest {
+            val model by
+                collectLastValue(
+                    kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE)
+                )
+            assertThat(model?.lastShortcutTriggeredTime).isNull()
+            underTest.updateShortcutTriggerTime(BACK_GESTURE)
+            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index 96b4b43..2b2c121 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -171,14 +171,15 @@
         }
 
     @Test
-    fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() =
+    fun longPressed_isA11yAction_doesNotShowMenu_opensSettings() =
         testScope.runTest {
-            createUnderTest(isOpenWppDirectlyEnabled = true)
+            createUnderTest()
             val isMenuVisible by collectLastValue(underTest.isMenuVisible)
             val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings)
+            val isA11yAction = true
             runCurrent()
 
-            underTest.onLongPress()
+            underTest.onLongPress(isA11yAction)
 
             assertThat(isMenuVisible).isFalse()
             assertThat(shouldOpenSettings).isTrue()
@@ -284,7 +285,6 @@
     private suspend fun createUnderTest(
         isLongPressFeatureEnabled: Boolean = true,
         isRevampedWppFeatureEnabled: Boolean = true,
-        isOpenWppDirectlyEnabled: Boolean = false,
     ) {
         // This needs to be re-created for each test outside of kosmos since the flag values are
         // read during initialization to set up flows. Maybe there is a better way to handle that.
@@ -298,7 +298,6 @@
                 featureFlags =
                     kosmos.fakeFeatureFlagsClassic.apply {
                         set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled)
-                        set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled)
                     },
                 broadcastDispatcher = fakeBroadcastDispatcher,
                 accessibilityManager = kosmos.accessibilityManagerWrapper,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index fc3b35d..a3959d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -1574,30 +1574,6 @@
         }
 
     @Test
-    @BrokenWithSceneContainer(339465026)
-    fun aodToOccluded() =
-        testScope.runTest {
-            // GIVEN a prior transition has run to AOD
-            runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD)
-            runCurrent()
-
-            // WHEN the keyguard is occluded
-            keyguardRepository.setKeyguardOccluded(true)
-            runCurrent()
-
-            // THEN a transition to OCCLUDED should occur
-            assertThat(transitionRepository)
-                .startedTransition(
-                    ownerName = "FromAodTransitionInteractor(isOccluded = true)",
-                    from = KeyguardState.AOD,
-                    to = KeyguardState.OCCLUDED,
-                    animatorAssertion = { it.isNotNull() },
-                )
-
-            coroutineContext.cancelChildren()
-        }
-
-    @Test
     @DisableSceneContainer
     fun aodToPrimaryBouncer() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
index be0d899..9e69601 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
@@ -18,8 +18,11 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+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
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -74,9 +77,9 @@
 
     @Test
     @DisableSceneContainer
+    @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun alpha_WhenNotGone_clockMigrationFlagIsOff_emitsKeyguardAlpha() =
         testScope.runTest {
-            mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
             val alpha by collectLastValue(underTest.alpha)
 
             keyguardTransitionRepository.sendTransitionSteps(
@@ -186,9 +189,9 @@
 
     @Test
     @DisableSceneContainer
+    @EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun alpha_whenGone_equalsZero() =
         testScope.runTest {
-            mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
             val alpha by collectLastValue(underTest.alpha)
 
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index 63d06a4..41c5b73 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -18,6 +18,8 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+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 as AConfigFlags
@@ -69,10 +71,11 @@
     private val burnInFlow = MutableStateFlow(BurnInModel())
 
     @Before
+    @DisableFlags(
+        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+    )
     fun setUp() {
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
-
         MockitoAnnotations.initMocks(this)
         whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
         kosmos.burnInInteractor = burnInInteractor
@@ -174,10 +177,9 @@
         }
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() =
         testScope.runTest {
-            mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
             burnInParameters =
                 burnInParameters.copy(
                     minViewY = 100,
@@ -226,10 +228,9 @@
         }
 
     @Test
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() =
         testScope.runTest {
-            mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
             burnInParameters =
                 burnInParameters.copy(
                     minViewY = 100,
@@ -310,104 +311,99 @@
         }
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_composeFlagOff_weatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = true,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = false
         )
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_composeFlagOff_weatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = true,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = false
         )
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_composeFlagOff_nonWeatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = false,
             expectedScaleOnly = true,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = false
         )
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
+    @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_composeFlagOff_nonWeatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = false,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = false
         )
 
     @Test
+    @EnableFlags(
+        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+    )
     fun translationAndScale_composeFlagOn_weatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = true,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = true
         )
 
     @Test
+    @EnableFlags(
+        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+    )
     fun translationAndScale_composeFlagOn_weatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = true,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = true
         )
 
     @Test
+    @EnableFlags(
+        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+    )
     fun translationAndScale_composeFlagOn_nonWeatherLargeClock() =
         testBurnInViewModelForClocks(
             isSmallClock = false,
             isWeatherClock = false,
             expectedScaleOnly = true,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = true
         )
 
     @Test
+    @EnableFlags(
+        AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+        AConfigFlags.FLAG_COMPOSE_LOCKSCREEN
+    )
     fun translationAndScale_composeFlagOn_nonWeatherSmallClock() =
         testBurnInViewModelForClocks(
             isSmallClock = true,
             isWeatherClock = false,
             expectedScaleOnly = false,
-            enableMigrateClocksToBlueprintFlag = true,
-            enableComposeLockscreenFlag = true
         )
 
     private fun testBurnInViewModelForClocks(
         isSmallClock: Boolean,
         isWeatherClock: Boolean,
         expectedScaleOnly: Boolean,
-        enableMigrateClocksToBlueprintFlag: Boolean,
-        enableComposeLockscreenFlag: Boolean
     ) =
         testScope.runTest {
-            if (enableMigrateClocksToBlueprintFlag) {
-                mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-            } else {
-                mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-            }
-
-            if (enableComposeLockscreenFlag) {
-                mSetFlagsRule.enableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
-            } else {
-                mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN)
-            }
             if (isSmallClock) {
                 keyguardClockRepository.setClockSize(ClockSize.SMALL)
                 // we need the following step to update stateFlow value
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
index 278c90a..fd2e335 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt
@@ -159,8 +159,9 @@
             )
 
             assertThat(values[0]).isEqualTo(1f)
-            // Should fade to zero between here
             assertThat(values[1]).isEqualTo(0f)
+            // Should always finish with 1f to show HUNs
+            assertThat(values[2]).isEqualTo(1f)
         }
 
     @Test
@@ -177,7 +178,7 @@
                 testScope,
             )
 
-            assertThat(values.size).isEqualTo(2)
+            assertThat(values.size).isEqualTo(3)
             // Shade stays open, and alpha should remain visible
             values.forEach { assertThat(it).isEqualTo(1f) }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
index 9b9e584..d5c9102 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -21,14 +21,23 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
 import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
 import com.google.common.truth.Truth
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -36,7 +45,33 @@
 class ModesTileUserActionInteractorTest : SysuiTestCase() {
     private val inputHandler = FakeQSTileIntentUserInputHandler()
 
-    val underTest = ModesTileUserActionInteractor(inputHandler)
+    @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
+    @Mock private lateinit var dialogDelegate: ModesDialogDelegate
+    @Mock private lateinit var mockDialog: SystemUIDialog
+
+    private lateinit var underTest: ModesTileUserActionInteractor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(dialogDelegate.createDialog()).thenReturn(mockDialog)
+
+        underTest =
+            ModesTileUserActionInteractor(
+                EmptyCoroutineContext,
+                inputHandler,
+                dialogTransitionAnimator,
+                dialogDelegate,
+            )
+    }
+
+    @Test
+    fun handleClick() = runTest {
+        underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false)))
+
+        verify(mockDialog).show()
+    }
 
     @Test
     fun handleLongClick() = runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
new file mode 100644
index 0000000..3baf2f4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.ui
+
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesTileMapperTest : SysuiTestCase() {
+    val config =
+        QSTileConfigTestBuilder.build {
+            uiConfig =
+                QSTileUIConfig.Resource(
+                    iconRes = R.drawable.qs_dnd_icon_off,
+                    labelRes = R.string.quick_settings_modes_label,
+                )
+        }
+
+    val underTest =
+        ModesTileMapper(
+            context.orCreateTestableResources
+                .apply {
+                    addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
+                    addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
+                }
+                .resources,
+            context.theme,
+        )
+
+    @Test
+    fun inactiveState() {
+        val model = ModesTileModel(isActivated = false)
+
+        val state = underTest.map(config, model)
+
+        assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
+        assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off)
+    }
+
+    @Test
+    fun activeState() {
+        val model = ModesTileModel(isActivated = true)
+
+        val state = underTest.map(config, model)
+
+        assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+        assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on)
+    }
+}
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 fd1b213..540a85a 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
@@ -175,12 +175,14 @@
             transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertThat(isVisible).isFalse()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = true)
+            )
             assertThat(isVisible).isTrue()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = false)
+            )
             assertThat(isVisible).isFalse()
         }
 
@@ -1559,6 +1561,63 @@
             verify(dismissCallback).onDismissCancelled()
         }
 
+    @Test
+    fun refreshLockscreenEnabled() =
+        testScope.runTest {
+            val transitionState =
+                prepareState(
+                    isDeviceUnlocked = true,
+                    initialSceneKey = Scenes.Gone,
+                )
+            underTest.start()
+            val isLockscreenEnabled by
+                collectLastValue(kosmos.deviceEntryInteractor.isLockscreenEnabled)
+            assertThat(isLockscreenEnabled).isTrue()
+
+            kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(false)
+            runCurrent()
+            // Pending value didn't propagate yet.
+            assertThat(isLockscreenEnabled).isTrue()
+
+            // Starting a transition to Lockscreen should refresh the value, causing the pending
+            // value
+            // to propagate to the real flow:
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Gone,
+                    toScene = Scenes.Lockscreen,
+                    currentScene = flowOf(Scenes.Gone),
+                    progress = flowOf(0.1f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+            assertThat(isLockscreenEnabled).isFalse()
+
+            kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(true)
+            runCurrent()
+            // Pending value didn't propagate yet.
+            assertThat(isLockscreenEnabled).isFalse()
+            transitionState.value = ObservableTransitionState.Idle(Scenes.Gone)
+            runCurrent()
+            assertThat(isLockscreenEnabled).isFalse()
+
+            // Starting another transition to Lockscreen should refresh the value, causing the
+            // pending
+            // value to propagate to the real flow:
+            transitionState.value =
+                ObservableTransitionState.Transition(
+                    fromScene = Scenes.Gone,
+                    toScene = Scenes.Lockscreen,
+                    currentScene = flowOf(Scenes.Gone),
+                    progress = flowOf(0.1f),
+                    isInitiatedByUserInput = false,
+                    isUserInputOngoing = flowOf(false),
+                )
+            runCurrent()
+            assertThat(isLockscreenEnabled).isTrue()
+        }
+
     private fun TestScope.emulateSceneTransition(
         transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
         toScene: SceneKey,
@@ -1642,8 +1701,8 @@
         return transitionStateFlow
     }
 
-    private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
-        setOf(
+    private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> =
+        listOf(
             fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
new file mode 100644
index 0000000..8810ade
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -0,0 +1,510 @@
+/*
+ * 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.app.Notification
+import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.modifyEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.StringSubject
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            testDispatcher = UnconfinedTestDispatcher()
+            statusBarStateController =
+                mock<SysuiStatusBarStateController>().also { mock ->
+                    doAnswer { statusBarState.ordinal }.whenever(mock).state
+                }
+            fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+        }
+    private val notifPipeline: NotifPipeline = mock()
+    private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
+
+    @Test
+    fun topUnseenSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun testPromoter() {
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val child3 = NotificationEntryBuilder().setTag("child3").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        GroupEntryBuilder()
+            .addChild(child1)
+            .addChild(child2)
+            .addChild(child3)
+            .setSummary(parent)
+            .build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(3).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+
+        runCoordinatorTest {
+            // TEST: base case - no entries in the list
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: none of these are unseen or ongoing yet, so don't pick them
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: when solo2 is the only one colorized, it gets picked up
+            solo2.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: once solo1 is colorized, it takes priority for being ranked higher
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: changing just the rank of solo1 causes it to pick up solo2 instead
+            solo1.modifyEntry { setRank(20) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching to SHADE disables the whole thing
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD picks up the same entry again
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating to not colorized revokes the top-ongoing status
+            solo2.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating the importance to LOW revokes top-ongoing status
+            solo1.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(4).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: Filtered out entries are ignored
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: top-ranked unseen child is selected (not the summary)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group))
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(child1.key)
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: no more colorized entries
+            solo1.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if the rank of solo1 is reduced, solo2 will be preferred
+            solo1.modifyEntry { setRank(3) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: switching to SHADE state will disable the entire selector
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD re-enables the selector
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: QS Expansion does not mark entries as seen
+            setShadeAndQsExpansionThenWait(0f, 1f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: Shade expansion does mark entries as seen
+            setShadeAndQsExpansionThenWait(1f, 0f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated while shade is expanded are NOT marked unseen
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated after shade is collapsed ARE marked unseen
+            setShadeAndQsExpansionThenWait(0f, 0f)
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: low importance disqualifies the entry for top unseen
+            solo2.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier_headsUpMarksSeen() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val listEntryList = listOf(solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2)
+
+        val hunRepo1 = solo1.fakeHeadsUpRowRepository()
+        val hunRepo2 = solo2.fakeHeadsUpRowRepository()
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: heads up state and waiting isn't enough to be seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value =
+                listOf(hunRepo1, hunRepo2)
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: even being pinned doesn't take effect immediately
+            hunRepo1.isPinned.value = true
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: after being pinned a full second, solo1 is seen
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: repeat; being heads up and pinned for 1 second triggers seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2)
+            hunRepo1.isPinned.value = false
+            hunRepo2.isPinned.value = true
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    private fun NotificationEntry.fakeHeadsUpRowRepository() =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any())
+
+    private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait(
+        shadeExpansion: Float,
+        qsExpansion: Float
+    ) {
+        kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion)
+        // The coordinator waits a fraction of a second for the shade expansion to stick.
+        testScheduler.advanceTimeBy(1.seconds)
+    }
+
+    private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT)
+
+    private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) {
+        kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline)
+        kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
+            KeyguardCoordinatorTestScope(
+                    kosmos.lockScreenMinimalismCoordinator,
+                    kosmos.testScope,
+                    kosmos.fakeSettings,
+                )
+                .testBlock()
+        }
+    }
+
+    private inner class KeyguardCoordinatorTestScope(
+        private val coordinator: LockScreenMinimalismCoordinator,
+        private val scope: TestScope,
+        private val fakeSettings: FakeSettings,
+    ) : CoroutineScope by scope {
+        fun assertThatTopOngoingKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topOngoingNotificationKey.value
+            )
+        }
+
+        fun assertThatTopUnseenKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topUnseenNotificationKey.value
+            )
+        }
+
+        val testScheduler: TestCoroutineScheduler
+            get() = scope.testScheduler
+
+        val promoter: NotifPromoter
+            get() = coordinator.unseenNotifPromoter
+
+        val topUnseenSectioner: NotifSectioner
+            get() = coordinator.topUnseenSectioner
+
+        val topOngoingSectioner: NotifSectioner
+            get() = coordinator.topOngoingSectioner
+
+        val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener =
+            argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) }
+                .lastValue
+
+        val collectionListener: NotifCollectionListener =
+            argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+        var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+            get() =
+                fakeSettings.getIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    UserHandle.USER_CURRENT,
+                ) == 1
+            set(value) {
+                fakeSettings.putIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    if (value) 1 else 2,
+                    UserHandle.USER_CURRENT,
+                )
+            }
+    }
+
+    companion object {
+
+        private fun NotificationEntry.setColorizedFgs(colorized: Boolean) {
+            sbn.notification.setColorizedFgs(colorized)
+        }
+
+        private fun Notification.setColorizedFgs(colorized: Boolean) {
+            extras.putBoolean(Notification.EXTRA_COLORIZED, colorized)
+            flags =
+                if (colorized) {
+                    flags or Notification.FLAG_FOREGROUND_SERVICE
+                } else {
+                    flags and Notification.FLAG_FOREGROUND_SERVICE.inv()
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
new file mode 100644
index 0000000..6ddc074
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt
@@ -0,0 +1,683 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.app.Notification
+import android.os.UserHandle
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.Kosmos
+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.setTransition
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.same
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+    private val kosmos = Kosmos()
+
+    private val headsUpManager: HeadsUpManager = mock()
+    private val keyguardRepository = FakeKeyguardRepository()
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val notifPipeline: NotifPipeline = mock()
+    private val statusBarStateController: StatusBarStateController = mock()
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Test
+    fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN: The keyguard goes away
+            keyguardRepository.setKeyguardShowing(false)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is shown regardless
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
+        // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(false)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The device transitions to AOD
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.AOD,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // THEN: We are no longer listening for shade expansions
+            verify(statusBarStateController, never()).addCallback(any())
+        }
+    }
+
+    @Test
+    fun unseenFilter_headsUpMarkedAsSeen() {
+        // GIVEN: Keyguard is not showing, shade is not expanded
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(false)
+        runKeyguardCoordinatorTest {
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+            )
+
+            // WHEN: A notification is posted
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: That notification is heads up
+            onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
+            testScheduler.runCurrent()
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
+            )
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN: The keyguard goes away
+            keyguardRepository.setKeyguardShowing(false)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE)
+            )
+
+            // THEN: The notification is shown regardless
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
+        // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry =
+                NotificationEntryBuilder()
+                    .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
+                    .build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "ongoing" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
+        // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry =
+                NotificationEntryBuilder().build().apply {
+                    row =
+                        mock<ExpandableNotificationRow>().apply {
+                            whenever(isMediaRow).thenReturn(true)
+                        }
+                }
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "media" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN: The filter is cleaned up
+            unseenFilter.onCleanup()
+
+            // THEN: The SeenNotificationProvider has been updated to reflect the suppression
+            assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue()
+        }
+    }
+
+    @Test
+    fun unseenFilterInvalidatesWhenSettingChanges() {
+        // GIVEN: Keyguard is not showing, and shade is expanded
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            // GIVEN: A notification is present
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // GIVEN: The setting for filtering unseen notifications is disabled
+            showOnlyUnseenNotifsOnKeyguardSetting = false
+
+            // GIVEN: The pipeline has registered the unseen filter for invalidation
+            val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
+            unseenFilter.setInvalidationListener(invalidationListener)
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is not filtered out
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+
+            // WHEN: The secure setting is changed
+            showOnlyUnseenNotifsOnKeyguardSetting = true
+
+            // THEN: The pipeline is invalidated
+            verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), any())
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+        }
+    }
+
+    @Test
+    fun unseenFilterAllowsNewNotif() {
+        // GIVEN: Keyguard is showing, no notifications present
+        keyguardRepository.setKeyguardShowing(true)
+        runKeyguardCoordinatorTest {
+            // WHEN: A new notification is posted
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // THEN: The notification is recognized as "unseen" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenFilterSeenGroupSummaryWithUnseenChild() {
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
+        runKeyguardCoordinatorTest {
+            // WHEN: A new notification is posted
+            val fakeSummary = NotificationEntryBuilder().build()
+            val fakeChild =
+                NotificationEntryBuilder()
+                    .setGroup(context, "group")
+                    .setGroupSummary(context, false)
+                    .build()
+            GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
+
+            collectionListener.onEntryAdded(fakeSummary)
+            collectionListener.onEntryAdded(fakeChild)
+
+            // WHEN: Keyguard is now showing, both notifications are marked as seen
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // WHEN: The child notification is now unseen
+            collectionListener.onEntryUpdated(fakeChild)
+
+            // THEN: The summary is not filtered out, because the child is unseen
+            assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
+        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: five seconds have passed
+            testScheduler.advanceTimeBy(5.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+            )
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
+            )
+
+            // THEN: The notification is now recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+        }
+    }
+
+    @Test
+    fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
+        // GIVEN: Keyguard is showing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: Keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
+            )
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is not recognized as "seen" and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
+        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
+            )
+            val firstEntry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(firstEntry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: another unseen notification is posted
+            val secondEntry = NotificationEntryBuilder().setId(2).build()
+            collectionListener.onEntryAdded(secondEntry)
+            testScheduler.runCurrent()
+
+            // WHEN: four more seconds have passed
+            testScheduler.advanceTimeBy(4.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Gone),
+                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+            )
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            kosmos.setTransition(
+                sceneTransition = Idle(Scenes.Lockscreen),
+                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
+            )
+
+            // THEN: The first notification is considered seen and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
+
+            // THEN: The second notification is still considered unseen and is not filtered out
+            assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: five more seconds have passed
+            testScheduler.advanceTimeBy(5.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is removed
+            collectionListener.onEntryRemoved(entry, 0)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is re-posted
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one more second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is removed
+            collectionListener.onEntryRemoved(entry, 0)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is re-posted
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one more second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
+    @Test
+    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
+        // GIVEN: Keyguard is showing, not dozing
+        keyguardRepository.setKeyguardShowing(true)
+        keyguardRepository.setIsDozing(false)
+        runKeyguardCoordinatorTest {
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: a new notification is posted
+            val entry = NotificationEntryBuilder().setId(1).build()
+            collectionListener.onEntryAdded(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: one second has passed
+            testScheduler.advanceTimeBy(1.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the notification is updated
+            collectionListener.onEntryUpdated(entry)
+            testScheduler.runCurrent()
+
+            // WHEN: four more seconds have passed
+            testScheduler.advanceTimeBy(4.seconds)
+            testScheduler.runCurrent()
+
+            // WHEN: the keyguard is no longer showing
+            keyguardRepository.setKeyguardShowing(false)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // WHEN: Keyguard is shown again
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GONE,
+                to = KeyguardState.LOCKSCREEN,
+                this.testScheduler,
+            )
+            testScheduler.runCurrent()
+
+            // THEN: The notification is considered unseen and is not filtered out.
+            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
+        }
+    }
+
+    private fun runKeyguardCoordinatorTest(
+        testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
+    ) {
+        val testDispatcher = UnconfinedTestDispatcher()
+        val testScope = TestScope(testDispatcher)
+        val fakeSettings =
+            FakeSettings().apply {
+                putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+            }
+        val seenNotificationsInteractor =
+            SeenNotificationsInteractor(ActiveNotificationListRepository())
+        val keyguardCoordinator =
+            OriginalUnseenKeyguardCoordinator(
+                testDispatcher,
+                mock<DumpManager>(),
+                headsUpManager,
+                keyguardRepository,
+                kosmos.keyguardTransitionInteractor,
+                KeyguardCoordinatorLogger(logcatLogBuffer()),
+                testScope.backgroundScope,
+                fakeSettings,
+                seenNotificationsInteractor,
+                statusBarStateController,
+            )
+        keyguardCoordinator.attach(notifPipeline)
+        testScope.runTest {
+            KeyguardCoordinatorTestScope(
+                    keyguardCoordinator,
+                    testScope,
+                    seenNotificationsInteractor,
+                    fakeSettings,
+                )
+                .testBlock()
+        }
+    }
+
+    private inner class KeyguardCoordinatorTestScope(
+        private val keyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+        private val scope: TestScope,
+        val seenNotificationsInteractor: SeenNotificationsInteractor,
+        private val fakeSettings: FakeSettings,
+    ) : CoroutineScope by scope {
+        val testScheduler: TestCoroutineScheduler
+            get() = scope.testScheduler
+
+        val unseenFilter: NotifFilter
+            get() = keyguardCoordinator.unseenNotifFilter
+
+        val collectionListener: NotifCollectionListener =
+            argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+        val onHeadsUpChangedListener: OnHeadsUpChangedListener
+            get() = argumentCaptor { verify(headsUpManager).addListener(capture()) }.lastValue
+
+        var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+            get() =
+                fakeSettings.getIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    UserHandle.USER_CURRENT,
+                ) == 1
+            set(value) {
+                fakeSettings.putIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    if (value) 1 else 2,
+                    UserHandle.USER_CURRENT,
+                )
+            }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+}
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 ce134e6..75ecb2c 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
@@ -41,8 +41,12 @@
 import com.android.systemui.communal.shared.model.CommunalScenes;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeAnimationRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
@@ -91,6 +95,7 @@
     @Mock private HeadsUpManager mHeadsUpManager;
     @Mock private VisibilityLocationProvider mVisibilityLocationProvider;
     @Mock private VisualStabilityProvider mVisualStabilityProvider;
+    @Mock private VisualStabilityCoordinatorLogger mLogger;
 
     @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor;
@@ -128,7 +133,9 @@
                 mVisibilityLocationProvider,
                 mVisualStabilityProvider,
                 mWakefulnessLifecycle,
-                mKosmos.getCommunalInteractor());
+                mKosmos.getCommunalInteractor(),
+                mKosmos.getKeyguardTransitionInteractor(),
+                mLogger);
         mCoordinator.attach(mNotifPipeline);
         mTestScope.getTestScheduler().runCurrent();
 
@@ -241,6 +248,38 @@
     }
 
     @Test
+    public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() {
+        // GIVEN the panel true expanded and device isn't pulsing
+        setFullyDozed(false);
+        setSleepy(false);
+        setLockscreenShowing(0.5f);
+        setPulsing(false);
+
+        // THEN group changes are NOT allowed
+        assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry));
+        assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry));
+
+        // THEN section changes are NOT allowed
+        assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry));
+    }
+
+    @Test
+    public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() {
+        // GIVEN the panel true expanded and device isn't pulsing
+        setFullyDozed(false);
+        setSleepy(false);
+        setLockscreenShowing(1.0f);
+        setPulsing(false);
+
+        // THEN group changes are NOT allowed
+        assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry));
+        assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry));
+
+        // THEN section changes are NOT allowed
+        assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry));
+    }
+
+    @Test
     public void testPulsing_screenOff_groupAndSectionChangesNotAllowed() {
         // GIVEN the device is pulsing and screen is off
         setFullyDozed(true);
@@ -614,7 +653,37 @@
     }
 
     private void setPanelExpanded(boolean expanded) {
-        mStatusBarStateListener.onExpandedChanged(expanded);
+        setPanelExpandedAndLockscreenShowing(expanded, /* lockscreenShowing = */ 0.0f);
     }
 
+    private void setLockscreenShowing(float lockscreenShowing) {
+        setPanelExpandedAndLockscreenShowing(/* panelExpanded = */ false, lockscreenShowing);
+    }
+
+    private void setPanelExpandedAndLockscreenShowing(boolean panelExpanded,
+            float lockscreenShowing) {
+        if (SceneContainerFlag.isEnabled()) {
+            mStatusBarStateListener.onExpandedChanged(panelExpanded);
+            mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava(
+                    mTestScope,
+                    makeLockscreenTransitionStep(lockscreenShowing),
+                    /* validateStep = */ false);
+        } else {
+            mStatusBarStateListener.onExpandedChanged(panelExpanded || lockscreenShowing > 0.0f);
+        }
+    }
+
+    private TransitionStep makeLockscreenTransitionStep(float value) {
+        if (value <= 0.0f) {
+            return new TransitionStep(KeyguardState.GONE);
+        } else if (value >= 1.0f) {
+            return new TransitionStep(KeyguardState.LOCKSCREEN);
+        } else {
+            return new TransitionStep(
+                    KeyguardState.GONE,
+                    KeyguardState.LOCKSCREEN,
+                    value,
+                    TransitionState.RUNNING);
+        }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..14134cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
index 5e87f46..61873ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewmodel
 
+import android.app.Notification
 import android.app.PendingIntent
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -90,7 +91,8 @@
         name: String = "example",
         timeRemaining: Duration = Duration.ofMinutes(3),
         resumeIntent: PendingIntent? = null,
-        resetIntent: PendingIntent? = null
+        addMinuteAction: Notification.Action? = null,
+        resetAction: Notification.Action? = null
     ) =
         TimerContentModel(
             icon = icon,
@@ -99,7 +101,8 @@
                 Paused(
                     timeRemaining = timeRemaining,
                     resumeIntent = resumeIntent,
-                    resetIntent = resetIntent,
+                    addMinuteAction = addMinuteAction,
+                    resetAction = resetAction,
                 )
         )
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f8e6337..f96cf10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -43,7 +43,6 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.statusbar.policy.fakeConfigurationController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 71cd95f..6f09931 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -1096,7 +1096,8 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            // Resets to 1f after communal scene is hidden
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1151,7 +1152,7 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1208,7 +1209,8 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            // Resets to 1f after communal scene is hidden
+            assertThat(alpha).isEqualTo(1f)
         }
 
     @Test
@@ -1263,7 +1265,7 @@
                 )
             )
             runCurrent()
-            assertThat(alpha).isEqualTo(0f)
+            assertThat(alpha).isEqualTo(1f)
         }
 
     private suspend fun TestScope.showLockscreen() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index b643968..c3c5a48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -50,6 +50,7 @@
                 statusBarStateController = statusBarStateController,
                 mainExecutor = mainExecutor,
                 legacyActivityStarter = { legacyActivityStarterInternal },
+                activityStarterInternal = { activityStarterInternal },
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 10a2f64..1797995 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -147,6 +147,48 @@
     }
 
     @Test
+    fun startActivityDismissingKeyguard_dismissShadeWhenOccluded_runAfterKeyguardGone() {
+        val intent = mock(Intent::class.java)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        `when`(keyguardStateController.isOccluded).thenReturn(true)
+        `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true))
+        `when`(communalSettingsInteractor.isCommunalFlagEnabled()).thenReturn(false)
+
+        underTest.startActivityDismissingKeyguard(intent, dismissShade = true)
+        mainExecutor.runAllReady()
+
+        val actionCaptor = argumentCaptor<OnDismissAction>()
+        verify(statusBarKeyguardViewManager)
+            .dismissWithAction(actionCaptor.capture(), any(), anyBoolean(), eq(null))
+        actionCaptor.firstValue.onDismiss()
+        mainExecutor.runAllReady()
+
+        verify(statusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any())
+    }
+
+    @Test
+    fun startActivityDismissingKeyguard_dismissShadeWhenOccluded_runImmediately() {
+        val intent = mock(Intent::class.java)
+        `when`(keyguardStateController.isShowing).thenReturn(true)
+        `when`(keyguardStateController.isOccluded).thenReturn(true)
+        `when`(communalSceneInteractor.isCommunalVisible).thenReturn(MutableStateFlow(true))
+        `when`(communalSettingsInteractor.isCommunalFlagEnabled()).thenReturn(true)
+
+        underTest.startActivityDismissingKeyguard(intent, dismissShade = true)
+        mainExecutor.runAllReady()
+
+        val actionCaptor = argumentCaptor<OnDismissAction>()
+        verify(statusBarKeyguardViewManager)
+            .dismissWithAction(actionCaptor.capture(), any(), anyBoolean(), eq(null))
+        actionCaptor.firstValue.onDismiss()
+        mainExecutor.runAllReady()
+
+        verify(statusBarKeyguardViewManager, never()).addAfterKeyguardGoneRunnable(any())
+        verify(activityTransitionAnimator)
+            .startIntentWithAnimation(eq(null), eq(false), eq(null), eq(false), any())
+    }
+
+    @Test
     fun startPendingIntentDismissingKeyguard_keyguardShowing_dismissWithAction() {
         val pendingIntent = mock(PendingIntent::class.java)
         `when`(pendingIntent.isActivity).thenReturn(true)
@@ -231,7 +273,6 @@
         // extra activity options to set on pending intent
         val activityOptions = mock(ActivityOptions::class.java)
         activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
-        activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
         val bundleCaptor = argumentCaptor<Bundle>()
 
         startPendingIntentMaybeDismissingKeyguard(
@@ -255,7 +296,8 @@
                 bundleCaptor.capture()
             )
         val options = ActivityOptions.fromBundle(bundleCaptor.firstValue)
-        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+        assertThat(options.getPendingIntentBackgroundActivityStartMode())
+            .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
         assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
     }
 
@@ -342,7 +384,6 @@
             )
     }
 
-    @EnableFlags(Flags.FLAG_COMMUNAL_HUB)
     @Test
     fun startPendingIntentDismissingKeyguard_transitionAnimator_animateCommunal() {
         val parent = FrameLayout(context)
@@ -389,7 +430,6 @@
             )
     }
 
-    @DisableFlags(Flags.FLAG_COMMUNAL_HUB)
     @Test
     fun startPendingIntentDismissingKeyguard_transitionAnimator_doNotAnimateCommunal() {
         val parent = FrameLayout(context)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 495ab61..8f9da3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -180,6 +180,23 @@
     }
 
     @Test
+    fun testDelete_untracked_runnableRuns() {
+        val headsUpEntry = createHeadsUpEntry(id = 0)
+
+        // None showing
+        mAvalancheController.headsUpEntryShowing = null
+
+        // Nothing is next
+        mAvalancheController.clearNext()
+
+        // Delete
+        mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel")
+
+        // Runnable was run
+        Mockito.verify(runnableMock, Mockito.times(1)).run()
+    }
+
+    @Test
     fun testDelete_isNext_removedFromNext_runnableNotRun() {
         // Entry is next
         val headsUpEntry = createHeadsUpEntry(id = 0)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index d0ddbff..b91bde4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -17,6 +17,7 @@
 
 import android.content.Context
 import android.os.Handler
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
@@ -33,6 +34,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
@@ -42,6 +44,7 @@
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.mockExecutorHandler
 import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.time.SystemClock
 import junit.framework.Assert
@@ -237,6 +240,36 @@
     }
 
     @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
+        hmp.showNotification(notifEntry)
+        Assert.assertTrue(notifEntry.isSeenInShade)
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
+        whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
+        val hmp = createHeadsUpManagerPhone()
+
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val row = mock<ExpandableNotificationRow>()
+        whenever(row.showingPulsing()).thenReturn(false)
+        notifEntry.row = row
+
+        hmp.showNotification(notifEntry)
+        Assert.assertFalse(notifEntry.isSeenInShade)
+    }
+
+    @Test
     fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() =
         testScope.runTest {
             // GIVEN
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
new file mode 100644
index 0000000..fdfc7f1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesDialogViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    val repository = kosmos.fakeZenModeRepository
+    val interactor = kosmos.zenModeInteractor
+
+    val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher)
+
+    @Test
+    fun tiles_filtersOutDisabledModes() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder().setName("Disabled").setEnabled(false).build(),
+                    TestModeBuilder.MANUAL_DND,
+                    TestModeBuilder()
+                        .setName("Enabled")
+                        .setEnabled(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Disabled with manual")
+                        .setEnabled(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(2)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Manual DND")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Enabled")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun tiles_filtersOutInactiveModesWithoutManualInvocation() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("Active without manual")
+                        .setActive(true)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Active with manual")
+                        .setTriggerDescription("trigger description")
+                        .setActive(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive with manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive without manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(3)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Active without manual")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Active with manual")
+                assertThat(this.subtext).isEqualTo("trigger description")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(2)!!) {
+                assertThat(this.text).isEqualTo("Inactive with manual")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun onClick_togglesTileState() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            val modeId = "id"
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setId(modeId)
+                        .setName("Test")
+                        .setManualInvocationAllowed(true)
+                        .build()
+                )
+            )
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(1)
+            assertThat(tiles?.elementAt(0)?.enabled).isFalse()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isTrue()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isFalse()
+        }
+}
diff --git a/packages/SystemUI/res/drawable/checkbox_circle_shape.xml b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml
new file mode 100644
index 0000000..2b987e2
--- /dev/null
+++ b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:bottom="12dp"
+        android:left="12dp"
+        android:right="12dp"
+        android:top="12dp">
+        <selector>
+            <item
+                android:drawable="@drawable/ic_check_circle_filled_24dp"
+                android:state_checked="true" />
+            <item
+                android:drawable="@drawable/ic_circle_outline_24dp"
+                android:state_checked="false" />
+        </selector>
+    </item>
+</layer-list>
diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml
new file mode 100644
index 0000000..16e2a3d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?androidprv:attr/materialColorPrimary"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c5.52,0 10,-4.48 10,-10S17.52,2 12,2zM10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66l1.41,1.41L10.59,16.6z" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml
new file mode 100644
index 0000000..82fa4f0
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?androidprv:attr/materialColorPrimary"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
+</vector>
diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml
index 5191895..d7b94ec 100644
--- a/packages/SystemUI/res/layout/app_clips_screenshot.xml
+++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml
@@ -58,9 +58,10 @@
         android:layout_width="wrap_content"
         android:layout_height="48dp"
         android:layout_marginStart="16dp"
+        android:button="@drawable/checkbox_circle_shape"
         android:checked="true"
         android:text="@string/backlinks_include_link"
-        android:textColor="?android:textColorSecondary"
+        android:textColor="?androidprv:attr/materialColorOnBackground"
         android:visibility="gone"
         app:layout_constraintBottom_toTopOf="@id/preview"
         app:layout_constraintStart_toEndOf="@id/cancel"
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
index f2bfbe5c9..3a679e3 100644
--- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -33,7 +33,6 @@
         android:id="@+id/icon"
         android:layout_width="24dp"
         android:layout_height="24dp"
-        android:src="@drawable/ic_close"
         app:tint="@android:color/white"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/label"
@@ -88,11 +87,10 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/mainButton"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/altButton"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
@@ -101,15 +99,23 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/altButton"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
-        android:drawablePadding="2dp"
-        android:drawableTint="@android:color/white"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
         app:layout_constraintStart_toEndOf="@id/mainButton"
+        app:layout_constraintEnd_toEndOf="@id/resetButton"
+        android:paddingEnd="4dp"
+        />
+
+    <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
+        android:id="@+id/resetButton"
+        android:layout_width="124dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+        app:layout_constraintStart_toEndOf="@id/altButton"
         app:layout_constraintEnd_toEndOf="parent"
         android:paddingEnd="4dp"
         />
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 763930d..07a40c8 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -33,9 +33,4 @@
     <!--  Whether the user switcher chip shows in the status bar. When true, the multi user
       avatar will no longer show on the lockscreen -->
     <bool name="flag_user_switcher_chip">false</bool>
-
-    <!-- Whether the battery icon is allowed to display a shield when battery life is being
-         protected. -->
-    <bool name="flag_battery_shield_icon">false</bool>
-
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2bd97d9..acc12d7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1084,6 +1084,21 @@
     <!-- QuickStep: Accessibility to toggle overview [CHAR LIMIT=40] -->
     <string name="quick_step_accessibility_toggle_overview">Toggle Overview</string>
 
+    <!-- Priority modes dialog title [CHAR LIMIT=35] -->
+    <string name="zen_modes_dialog_title">Priority modes</string>
+
+    <!-- Priority modes dialog confirmation button [CHAR LIMIT=15] -->
+    <string name="zen_modes_dialog_done">Done</string>
+
+    <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] -->
+    <string name="zen_modes_dialog_settings">Settings</string>
+
+    <!-- Priority modes: label for an active mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_on">On</string>
+
+    <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_off">Off</string>
+
     <!-- Zen mode: Priority only introduction message on first use -->
     <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string>
 
@@ -1249,6 +1264,8 @@
     <string name="communal_widget_picker_title">Lock screen widgets</string>
     <!-- Text displayed below the title in the communal widget picker providing additional details about the communal surface. [CHAR LIMIT=80] -->
     <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
+    <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_action_label_unselect_widget">unselect widget</string>
     <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
     <string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
     <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
@@ -1414,10 +1431,10 @@
     <string name="no_unseen_notif_text">No new notifications</string>
 
     <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=50] -->
-    <string name="adaptive_notification_edu_hun_title">Adaptive notifications is on</string>
+    <string name="adaptive_notification_edu_hun_title">Notification cooldown is on</string>
 
     <!-- Text of heads up notification for adaptive notifications user education. [CHAR LIMIT=100] -->
-    <string name="adaptive_notification_edu_hun_text">Your device now lowers the volume and reduces pop-ups on the screen for up to two minutes when you receive many notifications in a short time span.</string>
+    <string name="adaptive_notification_edu_hun_text">Your device volume and alerts are reduced automatically for up to 2 minutes when you get too many notifications at once.</string>
 
     <!-- Action label for going to adaptive notification settings [CHAR LIMIT=20] -->
     <string name="go_to_adaptive_notification_settings">Turn off</string>
@@ -3660,4 +3677,31 @@
     <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
     <!-- Label for volume undo action [CHAR LIMIT=NONE] -->
     <string name="volume_undo_action">Undo</string>
+
+    <!-- Keyboard touchpad contextual education strings-->
+    <!-- Education toast text for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_toast_content">To go back, swipe left or right with three fingers on the touchpad</string>
+    <!-- Education toast text for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_toast_content">To go home, swipe up with three fingers on the touchpad</string>
+    <!-- Education toast text for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_toast_content">To view recent apps, swipe up and hold with three fingers on the touchpad</string>
+    <!-- Education toast text for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_toast_content">To view all your apps, press the action key on your keyboard</string>
+
+    <!-- Education notification title for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_notification_title">Use your touchpad to go back</string>
+    <!-- Education notification text for Back [CHAR_LIMIT=100] -->
+    <string name="back_edu_notification_content">Swipe left or right using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_notification_title">Use your touchpad to go home</string>
+    <!-- Education notification text for Home [CHAR_LIMIT=100] -->
+    <string name="home_edu_notification_content">Swipe up using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_notification_title">Use your touchpad to view recent apps</string>
+    <!-- Education notification text for Overview [CHAR_LIMIT=100] -->
+    <string name="overview_edu_notification_content">Swipe up and hold using three fingers. Tap to learn more gestures.</string>
+    <!-- Education notification title for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string>
+    <!-- Education notification text for All Apps [CHAR_LIMIT=100] -->
+    <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 845ca5e..3019fe7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -344,7 +344,7 @@
      * Shows a voice session identified by {@code token}
      * @return true if the session was shown, false otherwise
      */
-    public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags,
+    public boolean showVoiceSession(IBinder token, @NonNull Bundle args, int flags,
             @Nullable String attributionTag) {
         IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface(
                 ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 484e758..4ef1f93 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -124,6 +124,8 @@
     public static final long SYSUI_STATE_SHORTCUT_HELPER_SHOWING = 1L << 32;
     // Touchpad gestures are disabled
     public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33;
+    // PiP animation is running
+    public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34;
     // Communal hub is showing
     public static final long SYSUI_STATE_COMMUNAL_HUB_SHOWING = 1L << 35;
 
@@ -175,6 +177,7 @@
             SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY,
             SYSUI_STATE_SHORTCUT_HELPER_SHOWING,
             SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED,
+            SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING,
             SYSUI_STATE_COMMUNAL_HUB_SHOWING,
     })
     public @interface SystemUiStateFlags {}
@@ -280,6 +283,9 @@
         if ((flags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) != 0) {
             str.add("touchpad_gestures_disabled");
         }
+        if ((flags & SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING) != 0) {
+            str.add("disable_gesture_pip_animating");
+        }
         if ((flags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) {
             str.add("communal_hub_showing");
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 10d1891..0f61233 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -34,6 +34,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
 import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor;
+import com.android.systemui.Flags;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.res.R;
@@ -130,7 +131,10 @@
                     verifyPasswordAndUnlock();
                 }
             });
-            okButton.setOnHoverListener(mLiftToActivateListener);
+
+            if (!Flags.simPinTalkbackFixForDoubleSubmit()) {
+                okButton.setOnHoverListener(mLiftToActivateListener);
+            }
         }
         if (pinInputFieldStyledFocusState()) {
             collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(),
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
index 6e5e44e..e9c9bc7 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java
@@ -363,6 +363,16 @@
     }
 
     @MainThread
+    void updateSettingsButtonStatus(int displayId,
+            @WindowMagnificationSettings.MagnificationSize int index) {
+        final MagnificationSettingsController magnificationSettingsController =
+                mMagnificationSettingsSupplier.get(displayId);
+        if (magnificationSettingsController != null) {
+            magnificationSettingsController.updateSettingsButtonStatusOnRestore(index);
+        }
+    }
+
+    @MainThread
     void toggleSettingsPanelVisibility(int displayId) {
         final MagnificationSettingsController magnificationSettingsController =
                 mMagnificationSettingsSupplier.get(displayId);
@@ -446,6 +456,11 @@
     @VisibleForTesting
     final WindowMagnifierCallback mWindowMagnifierCallback = new WindowMagnifierCallback() {
         @Override
+        public void onWindowMagnifierBoundsRestored(int displayId, int index) {
+            mHandler.post(() -> updateSettingsButtonStatus(displayId, index));
+        }
+
+        @Override
         public void onWindowMagnifierBoundsChanged(int displayId, Rect frame) {
             if (mMagnificationConnectionImpl != null) {
                 mMagnificationConnectionImpl.onWindowMagnifierBoundsChanged(displayId, frame);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index ed7062b..caf5517 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -100,6 +100,10 @@
         mWindowMagnificationSettings.toggleSettingsPanelVisibility();
     }
 
+    void updateSettingsButtonStatusOnRestore(@MagnificationSize int index) {
+        mWindowMagnificationSettings.updateSelectedButton(index);
+    }
+
     void closeMagnificationSettings() {
         mContext.unregisterComponentCallbacks(this);
         mWindowMagnificationSettings.hideSettingPanel();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index b37ba89..3828f9f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -127,6 +127,7 @@
     private final WindowManager mWm;
 
     private float mScale;
+    private int mSettingsButtonIndex = MagnificationSize.DEFAULT;
 
     /**
      * MagnificationFrame represents the bound of {@link #mMirrorSurfaceView} and is constrained
@@ -436,6 +437,7 @@
         if (!mMagnificationSizeScaleOptions.contains(index)) {
             return;
         }
+        mSettingsButtonIndex = index;
         int size = getMagnificationWindowSizeFromIndex(index);
         setWindowSize(size, size);
     }
@@ -446,6 +448,10 @@
         return (int) (initSize * scale) - (int) (initSize * scale) % 2;
     }
 
+    int getMagnificationFrameSizeFromIndex(@MagnificationSize int index) {
+        return getMagnificationWindowSizeFromIndex(index) - 2 * mMirrorSurfaceMargin;
+    }
+
     void setEditMagnifierSizeMode(boolean enable) {
         mEditSizeEnable = enable;
         applyResourcesValues();
@@ -457,8 +463,11 @@
 
         if (!enable) {
             // Keep the magnifier size when exiting edit mode
-            mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(
+            mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(
+                    mSettingsButtonIndex,
                     new Size(mMagnificationFrame.width(), mMagnificationFrame.height()));
+        } else {
+            mSettingsButtonIndex = MagnificationSize.CUSTOM;
         }
     }
 
@@ -944,7 +953,8 @@
     }
 
     private void setMagnificationFrame(int width, int height, int centerX, int centerY) {
-        mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(new Size(width, height));
+        mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(
+                mSettingsButtonIndex, new Size(width, height));
 
         // Sets the initial frame area for the mirror and place it to the given center on the
         // display.
@@ -954,6 +964,10 @@
     }
 
     private Size restoreMagnificationWindowFrameSizeIfPossible() {
+        if (Flags.saveAndRestoreMagnificationSettingsButtons()) {
+            return restoreMagnificationWindowFrameIndexAndSizeIfPossible();
+        }
+
         if (!mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity()) {
             return getDefaultMagnificationWindowFrameSize();
         }
@@ -961,8 +975,37 @@
         return mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity();
     }
 
+    private Size restoreMagnificationWindowFrameIndexAndSizeIfPossible() {
+        if (!mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity()) {
+            notifyWindowSizeRestored(MagnificationSize.DEFAULT);
+            return getDefaultMagnificationWindowFrameSize();
+        }
+
+        // This will return DEFAULT index if the stored preference is in an invalid format.
+        // Therefore, except CUSTOM, we would like to calculate the window width and height based
+        // on the restored MagnificationSize index.
+        int restoredIndex = mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity();
+        notifyWindowSizeRestored(restoredIndex);
+        if (restoredIndex == MagnificationSize.CUSTOM) {
+            return mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity();
+        }
+
+        int restoredSize = getMagnificationFrameSizeFromIndex(restoredIndex);
+        return new Size(restoredSize, restoredSize);
+    }
+
+    private void notifyWindowSizeRestored(@MagnificationSize int index) {
+        mSettingsButtonIndex = index;
+        if (isActivated()) {
+            // Send the callback only if the window magnification is activated. The check is to
+            // avoid updating the settings panel in the cases that window magnification is not yet
+            // activated such as during the constructor initialization of this class.
+            mWindowMagnifierCallback.onWindowMagnifierBoundsRestored(mDisplayId, index);
+        }
+    }
+
     private Size getDefaultMagnificationWindowFrameSize() {
-        final int defaultSize = getMagnificationWindowSizeFromIndex(MagnificationSize.MEDIUM)
+        final int defaultSize = getMagnificationWindowSizeFromIndex(MagnificationSize.DEFAULT)
                 - 2 * mMirrorSurfaceMargin;
         return new Size(defaultSize, defaultSize);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java
index e83e85e..ee36c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefs.java
@@ -16,10 +16,14 @@
 
 package com.android.systemui.accessibility;
 
+import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.util.Size;
 
+import com.android.systemui.Flags;
+
 /**
  * Class to handle SharedPreference for window magnification size.
  */
@@ -47,9 +51,15 @@
     /**
      * Saves the window frame size for current screen density.
      */
-    public void saveSizeForCurrentDensity(Size size) {
-        mWindowMagnificationSizePreferences.edit()
-                .putString(getKey(), size.toString()).apply();
+    public void saveIndexAndSizeForCurrentDensity(int index, Size size) {
+        if (Flags.saveAndRestoreMagnificationSettingsButtons()) {
+            mWindowMagnificationSizePreferences.edit()
+                    .putString(getKey(),
+                            WindowMagnificationFrameSpec.serialize(index, size)).apply();
+        } else {
+            mWindowMagnificationSizePreferences.edit()
+                    .putString(getKey(), size.toString()).apply();
+        }
     }
 
     /**
@@ -62,10 +72,32 @@
     }
 
     /**
+     * Gets the index preference for current screen density. Returns DEFAULT if no preference
+     * is found.
+     */
+    public @MagnificationSize int getIndexForCurrentDensity() {
+        final String spec = mWindowMagnificationSizePreferences.getString(getKey(), null);
+        if (spec == null) {
+            return MagnificationSize.DEFAULT;
+        }
+        try {
+            return WindowMagnificationFrameSpec.deserialize(spec).getIndex();
+        } catch (NumberFormatException e) {
+            return MagnificationSize.DEFAULT;
+        }
+    }
+
+    /**
      * Gets the size preference for current screen density.
      */
     public Size getSizeForCurrentDensity() {
-        return Size.parseSize(mWindowMagnificationSizePreferences.getString(getKey(), null));
+        if (Flags.saveAndRestoreMagnificationSettingsButtons()) {
+            return WindowMagnificationFrameSpec
+                    .deserialize(mWindowMagnificationSizePreferences.getString(getKey(), null))
+                    .getSize();
+        } else {
+            return Size.parseSize(mWindowMagnificationSizePreferences.getString(getKey(), null));
+        }
     }
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.kt b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.kt
new file mode 100644
index 0000000..c261a99
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationFrameSpec.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility
+
+import android.util.Size
+
+data class WindowMagnificationFrameSpec(val index: Int, val size: Size) {
+
+    companion object {
+        private fun throwInvalidWindowMagnificationFrameSpec(s: String?): Nothing {
+            throw NumberFormatException("Invalid WindowMagnificationFrameSpec: \"$s\"")
+        }
+
+        @JvmStatic fun serialize(index: Int, size: Size) = "$index,$size"
+
+        @JvmStatic
+        fun deserialize(s: String): WindowMagnificationFrameSpec {
+            val separatorIndex = s.indexOf(',')
+            if (separatorIndex < 0) {
+                throwInvalidWindowMagnificationFrameSpec(s)
+            }
+            return try {
+                WindowMagnificationFrameSpec(
+                    s.substring(0, separatorIndex).toInt(),
+                    Size.parseSize(s.substring(separatorIndex + 1))
+                )
+            } catch (e: NumberFormatException) {
+                throwInvalidWindowMagnificationFrameSpec(s)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 5f6f21a..99d966d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -58,6 +58,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.systemui.Flags;
 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView;
 import com.android.systemui.res.R;
 import com.android.systemui.util.settings.SecureSettings;
@@ -98,7 +99,7 @@
     private Button mDoneButton;
     private Button mEditButton;
     private ImageButton mFullScreenButton;
-    private int mLastSelectedButtonIndex = MagnificationSize.NONE;
+    private int mLastSelectedButtonIndex = MagnificationSize.DEFAULT;
 
     private boolean mAllowDiagonalScrolling = false;
 
@@ -115,19 +116,21 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
-            MagnificationSize.NONE,
+            MagnificationSize.CUSTOM,
             MagnificationSize.SMALL,
             MagnificationSize.MEDIUM,
             MagnificationSize.LARGE,
-            MagnificationSize.FULLSCREEN
+            MagnificationSize.FULLSCREEN,
+            MagnificationSize.DEFAULT
     })
     /** Denotes the Magnification size type. */
     public @interface MagnificationSize {
-        int NONE = 0;
+        int CUSTOM = 0;
         int SMALL = 1;
         int MEDIUM = 2;
         int LARGE = 3;
         int FULLSCREEN = 4;
+        int DEFAULT = MEDIUM;
     }
 
     @VisibleForTesting
@@ -445,13 +448,20 @@
     private void updateUIControlsIfNeeded() {
         int capability = getMagnificationCapability();
         int selectedButtonIndex = mLastSelectedButtonIndex;
+        WindowMagnificationFrameSizePrefs windowMagnificationFrameSizePrefs =
+                new WindowMagnificationFrameSizePrefs(mContext);
         switch (capability) {
             case ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW:
                 mEditButton.setVisibility(View.VISIBLE);
                 mAllowDiagonalScrollingView.setVisibility(View.VISIBLE);
                 mFullScreenButton.setVisibility(View.GONE);
                 if (selectedButtonIndex == MagnificationSize.FULLSCREEN) {
-                    selectedButtonIndex = MagnificationSize.NONE;
+                    if (Flags.saveAndRestoreMagnificationSettingsButtons()) {
+                        selectedButtonIndex =
+                                windowMagnificationFrameSizePrefs.getIndexForCurrentDensity();
+                    } else {
+                        selectedButtonIndex = MagnificationSize.CUSTOM;
+                    }
                 }
                 break;
 
@@ -613,7 +623,7 @@
 
     public void editMagnifierSizeMode(boolean enable) {
         setEditMagnifierSizeMode(enable);
-        updateSelectedButton(MagnificationSize.NONE);
+        updateSelectedButton(MagnificationSize.CUSTOM);
         hideSettingPanel();
     }
 
@@ -621,7 +631,7 @@
         if (index == MagnificationSize.FULLSCREEN) {
             // transit to fullscreen magnifier if needed
             transitToMagnificationMode(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
-        } else if (index != MagnificationSize.NONE) {
+        } else if (index != MagnificationSize.CUSTOM) {
             // update the window magnifier size
             mCallback.onSetMagnifierSize(index);
             // transit to window magnifier if needed
@@ -706,7 +716,7 @@
         });
     }
 
-    private void updateSelectedButton(@MagnificationSize int index) {
+    void updateSelectedButton(@MagnificationSize int index) {
         // Clear the state of last selected button
         if (mLastSelectedButtonIndex == MagnificationSize.SMALL) {
             mSmallButton.setSelected(false);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java
index a25e9a2..b4a2482 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.accessibility;
 
+import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize;
+
 import android.graphics.Rect;
 
 /**
@@ -68,4 +70,9 @@
      * @param displayId The logical display id.
      */
     void onClickSettingsButton(int displayId);
+
+    /**
+     * Called when restoring the magnification window size.
+     */
+    void onWindowMagnifierBoundsRestored(int displayId, @MagnificationSize int index);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
index f1fb45c..baca959 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
@@ -79,7 +79,8 @@
                 if (mCapture != null && mCapture) {
                     sendTouchEvent((MotionEvent) ev);
                 }
-                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
+                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP
+                        || ((MotionEvent) ev).getAction() == MotionEvent.ACTION_CANCEL) {
                     session.pop();
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 4035e95..efa55e9 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -128,8 +128,13 @@
                     completer.set(predecessor);
                 }
 
-                if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) {
-                    stopMonitoring(false);
+                if (mActiveTouchSessions.isEmpty()) {
+                    if (mStopMonitoringPending) {
+                        stopMonitoring(false);
+                    } else {
+                        // restart monitoring to reset any destructive state on the input session
+                        startMonitoring();
+                    }
                 }
             });
 
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 5c53234..e634726 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -88,7 +88,6 @@
     private boolean mPowerSaveEnabled;
     private boolean mIsBatteryDefender;
     private boolean mIsIncompatibleCharging;
-    private boolean mDisplayShieldEnabled;
     // Error state where we know nothing about the current battery state
     private boolean mBatteryStateUnknown;
     // Lazily-loaded since this is expected to be a rare-if-ever state
@@ -270,7 +269,7 @@
         int resId = 0;
         if (mPowerSaveEnabled) {
             resId = R.drawable.battery_unified_attr_powersave;
-        } else if (mIsBatteryDefender && mDisplayShieldEnabled) {
+        } else if (mIsBatteryDefender) {
             resId = R.drawable.battery_unified_attr_defend;
         } else if (isCharging) {
             resId = R.drawable.battery_unified_attr_charging;
@@ -288,7 +287,7 @@
     private ColorProfile getCurrentColorProfile() {
         return getColorProfile(
                 mPowerSaveEnabled,
-                mIsBatteryDefender && mDisplayShieldEnabled,
+                mIsBatteryDefender,
                 mPluggedIn,
                 mLevel <= 20);
     }
@@ -410,10 +409,6 @@
         mBatteryEstimateFetcher = fetcher;
     }
 
-    void setDisplayShieldEnabled(boolean displayShieldEnabled) {
-        mDisplayShieldEnabled = displayShieldEnabled;
-    }
-
     void updatePercentText() {
         if (!newStatusBarIcons()) {
             updatePercentTextLegacy();
@@ -659,7 +654,7 @@
         float mainBatteryWidth =
                 res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor;
 
-        boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender;
+        boolean displayShield = mIsBatteryDefender;
         float fullBatteryIconHeight =
                 BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield);
         float fullBatteryIconWidth =
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index 4f13e6f..9a30c21 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -34,7 +34,6 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -153,8 +152,6 @@
         mBatteryController = batteryController;
 
         mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString);
-        mView.setDisplayShieldEnabled(
-                getContext().getResources().getBoolean(R.bool.flag_battery_shield_icon));
 
         mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery);
         mSettingObserver = new SettingObserver(mMainHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index c95a94e..b10d37e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -34,7 +34,9 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.lightRevealMigration
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
+import com.android.systemui.biometrics.shared.model.toSensorType
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -102,6 +104,7 @@
 
     private var udfpsController: UdfpsController? = null
     private var udfpsRadius: Float = -1f
+    private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN
 
     override fun start() {
         init()
@@ -370,8 +373,11 @@
     private val udfpsControllerCallback =
         object : UdfpsController.Callback {
             override fun onFingerDown() {
-                // only show dwell ripple for device entry
-                if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                // only show dwell ripple for device entry non-ultrasonic udfps
+                if (
+                    keyguardUpdateMonitor.isFingerprintDetectionRunning &&
+                        udfpsType != FingerprintSensorType.UDFPS_ULTRASONIC
+                ) {
                     showDwellRipple()
                 }
             }
@@ -397,6 +403,7 @@
             if (it.size > 0) {
                 udfpsController = udfpsControllerProvider.get()
                 udfpsRadius = authController.udfpsRadius
+                udfpsType = it[0].sensorType.toSensorType()
 
                 if (mView.isAttachedToWindow) {
                     udfpsController?.addCallback(udfpsControllerCallback)
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 85e2bdb..b6ace81 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -19,10 +19,14 @@
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.os.Bundle
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewConfiguration
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import com.android.systemui.shade.TouchLogger
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -44,6 +48,10 @@
         attrs,
     ) {
 
+    init {
+        setupAccessibilityDelegate()
+    }
+
     constructor(
         context: Context,
         attrs: AttributeSet?,
@@ -55,6 +63,7 @@
             view: View,
             x: Int,
             y: Int,
+            isA11yAction: Boolean = false,
         )
 
         /** Notifies that the gesture was too short for a long press, it is actually a click. */
@@ -63,6 +72,8 @@
 
     var listener: Listener? = null
 
+    var accessibilityHintLongPressAction: AccessibilityAction? = null
+
     private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
         LongPressHandlingViewInteractionHandler(
             postDelayed = { block, timeoutMs ->
@@ -107,6 +118,51 @@
     override fun onTouchEvent(event: MotionEvent?): Boolean {
         return interactionHandler.onTouchEvent(event?.toModel())
     }
+
+    private fun setupAccessibilityDelegate() {
+        accessibilityDelegate =
+            object : AccessibilityDelegate() {
+                override fun onInitializeAccessibilityNodeInfo(
+                    v: View,
+                    info: AccessibilityNodeInfo
+                ) {
+                    super.onInitializeAccessibilityNodeInfo(v, info)
+                    if (
+                        interactionHandler.isLongPressHandlingEnabled &&
+                            accessibilityHintLongPressAction != null
+                    ) {
+                        info.addAction(accessibilityHintLongPressAction)
+                    }
+                }
+
+                override fun performAccessibilityAction(
+                    host: View,
+                    action: Int,
+                    args: Bundle?
+                ): Boolean {
+                    return if (
+                        interactionHandler.isLongPressHandlingEnabled &&
+                            action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK
+                    ) {
+                        val longPressHandlingView = host as? LongPressHandlingView
+                        if (longPressHandlingView != null) {
+                            // the coordinates are not available as it is an a11y long press
+                            listener?.onLongPressDetected(
+                                view = longPressHandlingView,
+                                x = 0,
+                                y = 0,
+                                isA11yAction = true,
+                            )
+                            true
+                        } else {
+                            false
+                        }
+                    } else {
+                        super.performAccessibilityAction(host, action, args)
+                    }
+                }
+            }
+    }
 }
 
 private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index 3d201a3..a445335 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.communal.dagger
 
 import android.content.Context
+import com.android.systemui.CoreStartable
 import com.android.systemui.communal.data.backup.CommunalBackupUtils
 import com.android.systemui.communal.data.db.CommunalDatabaseModule
 import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule
@@ -26,6 +27,7 @@
 import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
+import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.util.CommunalColors
 import com.android.systemui.communal.util.CommunalColorsImpl
@@ -40,6 +42,8 @@
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import kotlinx.coroutines.CoroutineScope
 
 @Module(
@@ -69,6 +73,13 @@
 
     @Binds fun bindCommunalColors(impl: CommunalColorsImpl): CommunalColors
 
+    @Binds
+    @IntoMap
+    @ClassKey(CommunalSceneTransitionInteractor::class)
+    abstract fun bindCommunalSceneTransitionInteractor(
+        impl: CommunalSceneTransitionInteractor
+    ): CoreStartable
+
     companion object {
         @Provides
         @Communal
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
index d8067b8..4de39c4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
@@ -49,14 +49,8 @@
     /** Whether the CTA tile has been dismissed. */
     fun isCtaDismissed(user: UserInfo): Flow<Boolean>
 
-    /** Whether the lock screen widget disclaimer has been dismissed by the user. */
-    fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean>
-
     /** Save the CTA tile dismissed state for the current user. */
     suspend fun setCtaDismissed(user: UserInfo)
-
-    /** Save the lock screen widget disclaimer dismissed state for the current user. */
-    suspend fun setDisclaimerDismissed(user: UserInfo)
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -74,9 +68,6 @@
     override fun isCtaDismissed(user: UserInfo): Flow<Boolean> =
         readKeyForUser(user, CTA_DISMISSED_STATE)
 
-    override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> =
-        readKeyForUser(user, DISCLAIMER_DISMISSED_STATE)
-
     /**
      * Emits an event each time a Backup & Restore restoration job is completed, and once at the
      * start of collection.
@@ -97,12 +88,6 @@
             logger.i("Dismissed CTA tile")
         }
 
-    override suspend fun setDisclaimerDismissed(user: UserInfo) =
-        withContext(bgDispatcher) {
-            getSharedPrefsForUser(user).edit().putBoolean(DISCLAIMER_DISMISSED_STATE, true).apply()
-            logger.i("Dismissed widget disclaimer")
-        }
-
     private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences {
         return userFileManager.getSharedPreferences(
             FILE_NAME,
@@ -124,6 +109,5 @@
         const val TAG = "CommunalPrefsRepository"
         const val FILE_NAME = "communal_hub_prefs"
         const val CTA_DISMISSED_STATE = "cta_dismissed"
-        const val DISCLAIMER_DISMISSED_STATE = "disclaimer_dismissed"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 7a4006d..260dcba 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -28,7 +28,6 @@
 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.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -52,7 +51,7 @@
     fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)
 
     /** Immediately snaps to the desired scene. */
-    fun snapToScene(toScene: SceneKey, delayMillis: Long = 0)
+    fun snapToScene(toScene: SceneKey)
 
     /**
      * Updates the transition state of the hub [SceneTransitionLayout].
@@ -93,11 +92,10 @@
         }
     }
 
-    override fun snapToScene(toScene: SceneKey, delayMillis: Long) {
+    override fun snapToScene(toScene: SceneKey) {
         applicationScope.launch {
             // SceneTransitionLayout state updates must be triggered on the thread the STL was
             // created on.
-            delay(delayMillis)
             sceneDataSource.snapToScene(toScene)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt
new file mode 100644
index 0000000..7d9e1df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.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.communal.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class CommunalSceneTransitionRepository @Inject constructor() {
+    /**
+     * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the
+     * next transition away from communal scene is started. It will be consumed exactly once and
+     * after that the state will be set back to null.
+     */
+    val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 3fffd76..dbddc23 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -23,6 +23,7 @@
 import android.os.UserHandle
 import android.os.UserManager
 import android.provider.Settings
+import com.android.app.tracing.coroutines.launch
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
@@ -64,10 +65,12 @@
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.emitOnStart
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.minutes
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -94,6 +97,7 @@
 @Inject
 constructor(
     @Application val applicationScope: CoroutineScope,
+    @Background private val bgScope: CoroutineScope,
     @Background val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     private val widgetRepository: CommunalWidgetRepository,
@@ -117,9 +121,25 @@
 
     private val _editModeOpen = MutableStateFlow(false)
 
-    /** Whether edit mode is currently open. */
+    /**
+     * Whether edit mode is currently open. This will be true from onCreate to onDestroy in
+     * [EditWidgetsActivity] and thus does not correspond to whether or not the activity is visible.
+     *
+     * Note that since this is called in onDestroy, it's not guaranteed to ever be set to false when
+     * edit mode is closed, such as in the case that a user exits edit mode manually with a back
+     * gesture or navigation gesture.
+     */
     val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow()
 
+    private val _editActivityShowing = MutableStateFlow(false)
+
+    /**
+     * Whether the edit mode activity is currently showing. This is true from onStart to onStop in
+     * [EditWidgetsActivity] so may be false even when the user is in edit mode, such as when a
+     * widget's individual configuration activity has launched.
+     */
+    val editActivityShowing: StateFlow<Boolean> = _editActivityShowing.asStateFlow()
+
     /** Whether communal features are enabled. */
     val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled
 
@@ -148,6 +168,17 @@
                 replay = 1,
             )
 
+    private val _isDisclaimerDismissed = MutableStateFlow(false)
+    val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
+
+    fun setDisclaimerDismissed() {
+        bgScope.launch("$TAG#setDisclaimerDismissed") {
+            _isDisclaimerDismissed.value = true
+            delay(DISCLAIMER_RESET_MILLIS)
+            _isDisclaimerDismissed.value = false
+        }
+    }
+
     /** Whether to show communal when exiting the occluded state. */
     val showCommunalFromOccluded: Flow<Boolean> =
         keyguardTransitionInteractor.startedKeyguardTransitionStep
@@ -301,6 +332,10 @@
         _editModeOpen.value = isOpen
     }
 
+    fun setEditActivityShowing(isOpen: Boolean) {
+        _editActivityShowing.value = isOpen
+    }
+
     /** Show the widget editor Activity. */
     fun showWidgetEditor(
         preselectedKey: String? = null,
@@ -510,6 +545,14 @@
     }
 
     companion object {
+        const val TAG = "CommunalInteractor"
+
+        /**
+         * The amount of time between showing the widget disclaimer to the user as measured from the
+         * moment the disclaimer is dimsissed.
+         */
+        val DISCLAIMER_RESET_MILLIS = 30.minutes
+
         /**
          * The user activity timeout which should be used when the communal hub is opened. A value
          * of -1 means that the user's chosen screen timeout will be used instead.
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
index 3517650..0b5f40d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.communal.domain.interactor
 
 import android.content.pm.UserInfo
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.communal.data.repository.CommunalPrefsRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -43,7 +42,7 @@
     private val repository: CommunalPrefsRepository,
     userInteractor: SelectedUserInteractor,
     private val userTracker: UserTracker,
-    @CommunalTableLog tableLogBuffer: TableLogBuffer
+    @CommunalTableLog tableLogBuffer: TableLogBuffer,
 ) {
 
     val isCtaDismissed: Flow<Boolean> =
@@ -64,25 +63,6 @@
     suspend fun setCtaDismissed(user: UserInfo = userTracker.userInfo) =
         repository.setCtaDismissed(user)
 
-    val isDisclaimerDismissed: Flow<Boolean> =
-        userInteractor.selectedUserInfo
-            .flatMapLatest { user -> repository.isDisclaimerDismissed(user) }
-            .logDiffsForTable(
-                tableLogBuffer = tableLogBuffer,
-                columnPrefix = "",
-                columnName = "isDisclaimerDismissed",
-                initialValue = false,
-            )
-            .stateIn(
-                scope = bgScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = false,
-            )
-
-    fun setDisclaimerDismissed(user: UserInfo = userTracker.userInfo) {
-        bgScope.launch("$TAG#setDisclaimerDismissed") { repository.setDisclaimerDismissed(user) }
-    }
-
     private companion object {
         const val TAG = "CommunalPrefsInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index 122f9647..aa9cbd0 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.domain.interactor
 
+import com.android.app.tracing.coroutines.launch
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
@@ -26,9 +27,11 @@
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.shared.model.KeyguardState
 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.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -39,6 +42,7 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
@@ -57,17 +61,48 @@
         _isLaunchingWidget.value = launching
     }
 
+    fun interface OnSceneAboutToChangeListener {
+        /** Notifies that the scene is about to change to [toScene]. */
+        fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?)
+    }
+
+    private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>()
+
+    /** Registers a listener which is called when the scene is about to change. */
+    fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) {
+        onSceneAboutToChangeListener.add(processor)
+    }
+
     /**
      * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
      * installed transition or the one specified by [transitionKey], if provided.
      */
-    fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) {
-        communalSceneRepository.changeScene(newScene, transitionKey)
+    fun changeScene(
+        newScene: SceneKey,
+        transitionKey: TransitionKey? = null,
+        keyguardState: KeyguardState? = null,
+    ) {
+        applicationScope.launch {
+            notifyListeners(newScene, keyguardState)
+            communalSceneRepository.changeScene(newScene, transitionKey)
+        }
     }
 
     /** Immediately snaps to the new scene. */
-    fun snapToScene(newScene: SceneKey, delayMillis: Long = 0) {
-        communalSceneRepository.snapToScene(newScene, delayMillis)
+    fun snapToScene(
+        newScene: SceneKey,
+        delayMillis: Long = 0,
+        keyguardState: KeyguardState? = null
+    ) {
+        applicationScope.launch("$TAG#snapToScene") {
+            delay(delayMillis)
+            notifyListeners(newScene, keyguardState)
+            communalSceneRepository.snapToScene(newScene)
+        }
+    }
+
+    private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) {
+        onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) }
     }
 
     /** Changes to Blank scene when starting an activity after dismissing keyguard. */
@@ -164,4 +199,8 @@
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = false,
             )
+
+    private companion object {
+        const val TAG = "CommunalSceneInteractor"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
new file mode 100644
index 0000000..8351566
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.communalSceneKtfRefactor
+import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepository
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.util.kotlin.pairwise
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/**
+ * This class listens to [SceneTransitionLayout] transitions and manages keyguard transition
+ * framework (KTF) states accordingly for communal states.
+ *
+ * There are a few rules:
+ * - There are only 2 communal scenes: [CommunalScenes.Communal] and [CommunalScenes.Blank]
+ * - When scene framework is on [CommunalScenes.Blank], KTF is allowed to change its scenes freely
+ * - When scene framework is on [CommunalScenes.Communal], KTF is locked into
+ *   [KeyguardState.GLANCEABLE_HUB]
+ */
+@SysUISingleton
+class CommunalSceneTransitionInteractor
+@Inject
+constructor(
+    val transitionInteractor: KeyguardTransitionInteractor,
+    val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
+    private val settingsInteractor: CommunalSettingsInteractor,
+    @Application private val applicationScope: CoroutineScope,
+    private val sceneInteractor: CommunalSceneInteractor,
+    private val repository: CommunalSceneTransitionRepository,
+    keyguardInteractor: KeyguardInteractor,
+) : CoreStartable, CommunalSceneInteractor.OnSceneAboutToChangeListener {
+
+    private var currentTransitionId: UUID? = null
+    private var progressJob: Job? = null
+
+    private val currentToState: KeyguardState
+        get() = internalTransitionInteractor.currentTransitionInfoInternal.value.to
+
+    /**
+     * The next keyguard state to trigger when exiting [CommunalScenes.Communal]. This is only used
+     * if the state is changed by user gesture or not explicitly defined by the caller when changing
+     * scenes programmatically.
+     *
+     * This is needed because we do not always want to exit back to the KTF state we came from. For
+     * example, when going from HUB (Communal) -> OCCLUDED (Blank) -> HUB (Communal) and then
+     * closing the hub via gesture, we don't want to go back to OCCLUDED but instead either go to
+     * DREAM or LOCKSCREEN depending on if there is a dream showing.
+     */
+    private val nextKeyguardStateInternal =
+        combine(
+            keyguardInteractor.isDreaming,
+            keyguardInteractor.isKeyguardOccluded,
+            keyguardInteractor.isKeyguardGoingAway,
+        ) { dreaming, occluded, keyguardGoingAway ->
+            if (keyguardGoingAway) {
+                KeyguardState.GONE
+            } else if (dreaming) {
+                KeyguardState.DREAMING
+            } else if (occluded) {
+                KeyguardState.OCCLUDED
+            } else {
+                KeyguardState.LOCKSCREEN
+            }
+        }
+
+    private val nextKeyguardState: StateFlow<KeyguardState> =
+        combine(
+                repository.nextLockscreenTargetState,
+                nextKeyguardStateInternal,
+            ) { override, nextState ->
+                override ?: nextState
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = KeyguardState.LOCKSCREEN,
+            )
+
+    override fun start() {
+        if (
+            communalSceneKtfRefactor() &&
+                settingsInteractor.isCommunalFlagEnabled() &&
+                !SceneContainerFlag.isEnabled
+        ) {
+            sceneInteractor.registerSceneStateProcessor(this)
+            listenForSceneTransitionProgress()
+        }
+    }
+
+    /**
+     * Called when the scene is programmatically changed, allowing callers to specify which KTF
+     * state should be set when transitioning to [CommunalScenes.Blank]
+     */
+    override fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) {
+        if (toScene != CommunalScenes.Blank || keyguardState == null) return
+        repository.nextLockscreenTargetState.value = keyguardState
+    }
+
+    /** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */
+    private fun listenForSceneTransitionProgress() {
+        applicationScope.launch {
+            sceneInteractor.transitionState
+                .pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank))
+                .collect { (prevTransition, transition) ->
+                    when (transition) {
+                        is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition)
+                        is ObservableTransitionState.Transition ->
+                            handleTransition(prevTransition, transition)
+                    }
+                }
+        }
+    }
+
+    private suspend fun handleIdle(
+        prevTransition: ObservableTransitionState,
+        idle: ObservableTransitionState.Idle
+    ) {
+        if (
+            prevTransition is ObservableTransitionState.Transition &&
+                currentTransitionId != null &&
+                idle.currentScene == prevTransition.toScene
+        ) {
+            finishCurrentTransition()
+        } else {
+            // We may receive an Idle event without a corresponding Transition
+            // event, such as when snapping to a scene without an animation.
+            val targetState =
+                if (idle.currentScene == CommunalScenes.Blank) {
+                    nextKeyguardState.value
+                } else {
+                    KeyguardState.GLANCEABLE_HUB
+                }
+            transitionKtfTo(targetState)
+            repository.nextLockscreenTargetState.value = null
+        }
+    }
+
+    private fun finishCurrentTransition() {
+        internalTransitionInteractor.updateTransition(
+            currentTransitionId!!,
+            1f,
+            TransitionState.FINISHED
+        )
+        resetTransitionData()
+    }
+
+    private suspend fun finishReversedTransitionTo(state: KeyguardState) {
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = internalTransitionInteractor.currentTransitionInfoInternal.value.to,
+                to = state,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.REVERSE
+            )
+        currentTransitionId = internalTransitionInteractor.startTransition(newTransition)
+        internalTransitionInteractor.updateTransition(
+            currentTransitionId!!,
+            1f,
+            TransitionState.FINISHED
+        )
+        resetTransitionData()
+    }
+
+    private fun resetTransitionData() {
+        progressJob?.cancel()
+        progressJob = null
+        currentTransitionId = null
+    }
+
+    private suspend fun handleTransition(
+        prevTransition: ObservableTransitionState,
+        transition: ObservableTransitionState.Transition
+    ) {
+        if (prevTransition.isTransitioning(from = transition.fromScene, to = transition.toScene)) {
+            // This is a new transition, but exactly the same as the previous state. Skip resetting
+            // KTF for this case and just collect the new progress instead.
+            collectProgress(transition)
+        } else if (transition.toScene == CommunalScenes.Communal) {
+            if (currentTransitionId != null) {
+                if (currentToState == KeyguardState.GLANCEABLE_HUB) {
+                    transitionKtfTo(transitionInteractor.getStartedFromState())
+                }
+            }
+            startTransitionToGlanceableHub()
+            collectProgress(transition)
+        } else if (transition.toScene == CommunalScenes.Blank) {
+            if (currentTransitionId != null) {
+                // Another transition started before this one is completed. Transition to the
+                // GLANCEABLE_HUB state so that we can properly transition away from it.
+                transitionKtfTo(KeyguardState.GLANCEABLE_HUB)
+            }
+            startTransitionFromGlanceableHub()
+            collectProgress(transition)
+        }
+    }
+
+    private suspend fun transitionKtfTo(state: KeyguardState) {
+        val currentTransition = transitionInteractor.transitionState.value
+        if (currentTransition.isFinishedIn(state)) {
+            // This is already the state we want to be in
+            resetTransitionData()
+        } else if (currentTransition.isTransitioning(to = state)) {
+            finishCurrentTransition()
+        } else {
+            finishReversedTransitionTo(state)
+        }
+    }
+
+    private fun collectProgress(transition: ObservableTransitionState.Transition) {
+        progressJob?.cancel()
+        progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } }
+    }
+
+    private suspend fun startTransitionFromGlanceableHub() {
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = nextKeyguardState.value,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.RESET,
+            )
+        repository.nextLockscreenTargetState.value = null
+        startTransition(newTransition)
+    }
+
+    private suspend fun startTransitionToGlanceableHub() {
+        val currentState = internalTransitionInteractor.currentTransitionInfoInternal.value.to
+        val newTransition =
+            TransitionInfo(
+                ownerName = this::class.java.simpleName,
+                from = currentState,
+                to = KeyguardState.GLANCEABLE_HUB,
+                animator = null,
+                modeOnCanceled = TransitionModeOnCanceled.RESET,
+            )
+        startTransition(newTransition)
+    }
+
+    private suspend fun startTransition(transitionInfo: TransitionInfo) {
+        if (currentTransitionId != null) {
+            resetTransitionData()
+        }
+        currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo)
+    }
+
+    private fun updateProgress(progress: Float) {
+        if (currentTransitionId == null) return
+        internalTransitionInteractor.updateTransition(
+            currentTransitionId!!,
+            progress.coerceIn(0f, 1f),
+            TransitionState.RUNNING
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 19d7ceb..01ed2b7 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.EditModeState
 import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.media.controls.ui.view.MediaHost
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -75,8 +76,16 @@
         communalInteractor.signalUserInteraction()
     }
 
-    fun changeScene(scene: SceneKey, transitionKey: TransitionKey? = null) {
-        communalSceneInteractor.changeScene(scene, transitionKey)
+    /**
+     * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
+     * installed transition or the one specified by [transitionKey], if provided.
+     */
+    fun changeScene(
+        scene: SceneKey,
+        transitionKey: TransitionKey? = null,
+        keyguardState: KeyguardState? = null
+    ) {
+        communalSceneInteractor.changeScene(scene, transitionKey, keyguardState)
     }
 
     fun setEditModeState(state: EditModeState?) = communalSceneInteractor.setEditModeState(state)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 7b0aadf..830f543 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -24,7 +24,6 @@
 import android.util.Log
 import androidx.activity.result.ActivityResultLauncher
 import com.android.internal.logging.UiEventLogger
-import com.android.systemui.Flags.enableWidgetPickerSizeFilter
 import com.android.systemui.communal.data.model.CommunalWidgetCategories
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalPrefsInteractor
@@ -82,10 +81,10 @@
         communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING }
 
     val showDisclaimer: Flow<Boolean> =
-        allOf(isCommunalContentVisible, not(communalPrefsInteractor.isDisclaimerDismissed))
+        allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed))
 
     fun onDisclaimerDismissed() {
-        communalPrefsInteractor.setDisclaimerDismissed()
+        communalInteractor.setDisclaimerDismissed()
     }
 
     /**
@@ -176,16 +175,14 @@
 
         return Intent(Intent.ACTION_PICK).apply {
             setPackage(packageName)
-            if (enableWidgetPickerSizeFilter()) {
-                putExtra(
-                    EXTRA_DESIRED_WIDGET_WIDTH,
-                    resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
-                )
-                putExtra(
-                    EXTRA_DESIRED_WIDGET_HEIGHT,
-                    resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
-                )
-            }
+            putExtra(
+                EXTRA_DESIRED_WIDGET_WIDTH,
+                resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
+            )
+            putExtra(
+                EXTRA_DESIRED_WIDGET_HEIGHT,
+                resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
+            )
             putExtra(
                 AppWidgetManager.EXTRA_CATEGORY_FILTER,
                 CommunalWidgetCategories.defaultCategories
@@ -217,6 +214,14 @@
     /** Sets whether edit mode is currently open */
     fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
 
+    /**
+     * Sets whether the edit mode activity is currently showing.
+     *
+     * See [CommunalInteractor.editActivityShowing] for more info.
+     */
+    fun setEditActivityShowing(showing: Boolean) =
+        communalInteractor.setEditActivityShowing(showing)
+
     /** Called when exiting the edit mode, before transitioning back to the communal scene. */
     fun cleanupEditModeState() {
         communalSceneInteractor.setEditModeState(null)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt
index 4efaf87..0844462 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalTransitionAnimatorController.kt
@@ -37,13 +37,21 @@
         delegate.onIntentStarted(willAnimate)
     }
 
+    override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+        delegate.onTransitionAnimationStart(isExpandingFullyAbove)
+        // TODO(b/330672236): move this to onTransitionAnimationEnd() without the delay.
+        communalSceneInteractor.snapToScene(
+            CommunalScenes.Blank,
+            ActivityTransitionAnimator.TIMINGS.totalDuration
+        )
+    }
+
     override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
         communalSceneInteractor.setIsLaunchingWidget(false)
         delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
     }
 
     override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
-        communalSceneInteractor.snapToScene(CommunalScenes.Blank)
         communalSceneInteractor.setIsLaunchingWidget(false)
         delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 46f802f..08fe42e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -96,8 +96,7 @@
                                 run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
                             }
                         }
-                    }
-                        ?: run { Log.w(TAG, "No data in result.") }
+                    } ?: run { Log.w(TAG, "No data in result.") }
                 }
                 else ->
                     Log.w(
@@ -195,6 +194,8 @@
     override fun onStart() {
         super.onStart()
 
+        communalViewModel.setEditActivityShowing(true)
+
         if (shouldOpenWidgetPickerOnStart) {
             onOpenWidgetPicker()
             shouldOpenWidgetPickerOnStart = false
@@ -206,6 +207,7 @@
 
     override fun onStop() {
         super.onStop()
+        communalViewModel.setEditActivityShowing(false)
 
         logger.i("Stopping the communal widget editor activity")
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 7f8103e..6864f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -18,7 +18,7 @@
 
 import android.app.Activity
 import android.app.ActivityOptions
-import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
 import android.app.Dialog
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -93,8 +93,8 @@
                 0 /* enterResId */,
                 0 /* exitResId */
             ).apply {
-                pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+                pendingIntentBackgroundActivityStartMode =
+                    MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
                 taskAlwaysOnTop = true
             }
 
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 e2ad774..3f937bb 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
@@ -13,8 +13,10 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withContext
 
@@ -25,7 +27,7 @@
      * chosen any secure authentication method and even if they set the lockscreen to be dismissed
      * when the user swipes on it.
      */
-    suspend fun isLockscreenEnabled(): Boolean
+    val isLockscreenEnabled: StateFlow<Boolean>
 
     /**
      * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
@@ -39,6 +41,13 @@
      * the lockscreen.
      */
     val isBypassEnabled: StateFlow<Boolean>
+
+    /**
+     * 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
+     * when the user swipes on it.
+     */
+    suspend fun isLockscreenEnabled(): Boolean
 }
 
 /** Encapsulates application state for device entry. */
@@ -53,12 +62,8 @@
     private val keyguardBypassController: KeyguardBypassController,
 ) : DeviceEntryRepository {
 
-    override suspend fun isLockscreenEnabled(): Boolean {
-        return withContext(backgroundDispatcher) {
-            val selectedUserId = userRepository.getSelectedUserInfo().id
-            !lockPatternUtils.isLockScreenDisabled(selectedUserId)
-        }
-    }
+    private val _isLockscreenEnabled = MutableStateFlow(true)
+    override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow()
 
     override val isBypassEnabled: StateFlow<Boolean> =
         conflatedCallbackFlow {
@@ -78,6 +83,15 @@
                 SharingStarted.Eagerly,
                 initialValue = keyguardBypassController.bypassEnabled,
             )
+
+    override suspend fun isLockscreenEnabled(): Boolean {
+        return withContext(backgroundDispatcher) {
+            val selectedUserId = userRepository.getSelectedUserInfo().id
+            val isEnabled = !lockPatternUtils.isLockScreenDisabled(selectedUserId)
+            _isLockscreenEnabled.value = isEnabled
+            isEnabled
+        }
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index ea0e59b..9b95ac4 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -28,12 +28,14 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
@@ -101,6 +103,10 @@
                 initialValue = false,
             )
 
+    val isLockscreenEnabled: Flow<Boolean> by lazy {
+        repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() }
+    }
+
     /**
      * Whether it's currently possible to swipe up to enter the device without requiring
      * authentication or when the device is already authenticated using a passive authentication
@@ -115,14 +121,14 @@
      */
     val canSwipeToEnter: StateFlow<Boolean?> =
         combine(
-                // This is true when the user has chosen to show the lockscreen but has not made it
-                // secure.
                 authenticationInteractor.authenticationMethod.map {
-                    it == AuthenticationMethodModel.None && repository.isLockscreenEnabled()
+                    it == AuthenticationMethodModel.None
                 },
+                isLockscreenEnabled,
                 deviceUnlockedInteractor.deviceUnlockStatus,
                 isDeviceEntered
-            ) { isSwipeAuthMethod, deviceUnlockStatus, isDeviceEntered ->
+            ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered ->
+                val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled
                 (isSwipeAuthMethod ||
                     (deviceUnlockStatus.isUnlocked &&
                         deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) &&
@@ -186,6 +192,17 @@
     }
 
     /**
+     * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest
+     * value.
+     *
+     * Without calling this method, the flow will have a stale value unless the collector is removed
+     * and re-added.
+     */
+    suspend fun refreshLockscreenEnabled() {
+        isLockscreenEnabled()
+    }
+
+    /**
      * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
      * dismissed once the authentication challenge is completed. For example, completing a biometric
      * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 3294c81..b45ebd8 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -256,6 +256,7 @@
                 it.cancel()
                 null
             }
+        mOverlayStateController.setExitAnimationsRunning(false)
     }
 
     private fun blurAnimator(
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 53b9261..0e2e2e6 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -16,12 +16,21 @@
 
 package com.android.systemui.education.dagger
 
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.education.data.repository.ContextualEducationRepository
 import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl
+import com.android.systemui.education.domain.interactor.ContextualEducationInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
+import com.android.systemui.shared.education.GestureType
 import dagger.Binds
+import dagger.Lazy
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import java.time.Clock
 import javax.inject.Qualifier
 import kotlinx.coroutines.CoroutineDispatcher
@@ -53,5 +62,41 @@
         fun provideEduClock(): Clock {
             return Clock.systemUTC()
         }
+
+        @Provides
+        @IntoMap
+        @ClassKey(ContextualEducationInteractor::class)
+        fun provideContextualEducationInteractor(
+            implLazy: Lazy<ContextualEducationInteractor>
+        ): CoreStartable {
+            return if (Flags.keyboardTouchpadContextualEducation()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                return NoOpCoreStartable
+            }
+        }
+
+        @Provides
+        fun provideKeyboardTouchpadEduStatsInteractor(
+            implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl>
+        ): KeyboardTouchpadEduStatsInteractor {
+            return if (Flags.keyboardTouchpadContextualEducation()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                return NoOpKeyboardTouchpadEduStatsInteractor
+            }
+        }
+    }
+
+    private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
+        override fun incrementSignalCount(gestureType: GestureType) {}
+
+        override fun updateShortcutTriggerTime(gestureType: GestureType) {}
+    }
+
+    private object NoOpCoreStartable : CoreStartable {
+        override fun start() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
new file mode 100644
index 0000000..e2aa911
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 com.android.systemui.education.domain.interactor
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.education.data.repository.ContextualEducationRepository
+import com.android.systemui.shared.education.GestureType
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * Allows updating education data (e.g. signal count, shortcut time) for different gesture types.
+ * Change user education repository when user is changed.
+ */
+@SysUISingleton
+class ContextualEducationInteractor
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val repository: ContextualEducationRepository,
+) : CoreStartable {
+
+    override fun start() {
+        backgroundScope.launch {
+            selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) }
+        }
+    }
+
+    suspend fun incrementSignalCount(gestureType: GestureType) =
+        repository.incrementSignalCount(gestureType)
+
+    suspend fun updateShortcutTriggerTime(gestureType: GestureType) =
+        repository.updateShortcutTriggerTime(gestureType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
new file mode 100644
index 0000000..643e571
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.systemui.education.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.education.GestureType
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is
+ * for having a different implementation of interactor when the feature flag is off.
+ */
+interface KeyboardTouchpadEduStatsInteractor {
+    fun incrementSignalCount(gestureType: GestureType)
+
+    fun updateShortcutTriggerTime(gestureType: GestureType)
+}
+
+/** Allow update to education data related to keyboard/touchpad. */
+@SysUISingleton
+class KeyboardTouchpadEduStatsInteractorImpl
+@Inject
+constructor(
+    @Background private val backgroundScope: CoroutineScope,
+    private val contextualEducationInteractor: ContextualEducationInteractor
+) : KeyboardTouchpadEduStatsInteractor {
+
+    override fun incrementSignalCount(gestureType: GestureType) {
+        // Todo: check if keyboard/touchpad is connected before update
+        backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) }
+    }
+
+    override fun updateShortcutTriggerTime(gestureType: GestureType) {
+        backgroundScope.launch {
+            contextualEducationInteractor.updateShortcutTriggerTime(gestureType)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index af7ecf6..0e06117 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
+import com.android.systemui.qs.flags.NewQsUI
+import com.android.systemui.qs.flags.QSComposeFragment
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
@@ -35,6 +37,8 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -53,6 +57,7 @@
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
+        NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
 
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
@@ -66,14 +71,20 @@
 
         // DualShade dependencies
         DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag()
+
+        // QS Fragment using Compose dependencies
+        QSComposeFragment.token dependsOn NewQsUI.token
     }
 
     private inline val politeNotifications
         get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
+
     private inline val crossAppPoliteNotifications
         get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
+
     private inline val vibrateWhileUnlockedToken: FlagToken
         get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 3d3584e..d0beb7a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -137,12 +137,6 @@
     // TODO(b/267722622): Tracking Bug
     @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag("wallpaper_picker_ui_for_aiwp")
 
-    /** Whether to allow long-press on the lock screen to directly open wallpaper picker. */
-    // TODO(b/277220285): Tracking bug.
-    @JvmField
-    val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP =
-        unreleasedFlag("lock_screen_long_press_directly_opens_wallpaper_picker")
-
     /** Whether page transition animations in the wallpaper picker are enabled */
     // TODO(b/291710220): Tracking bug.
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 893835a..59ec87a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
+import android.util.Log
 import com.android.app.animation.Interpolators
 import com.android.app.tracing.coroutines.launch
 import com.android.systemui.dagger.SysUISingleton
@@ -184,11 +185,7 @@
                 .filterRelevantKeyguardStateAnd { isOccluded -> isOccluded }
                 .collect {
                     if (!maybeHandleInsecurePowerGesture()) {
-                        startTransitionTo(
-                            toState = KeyguardState.OCCLUDED,
-                            modeOnCanceled = TransitionModeOnCanceled.RESET,
-                            ownerReason = "isOccluded = true",
-                        )
+                        Log.i(TAG, "Ignoring change to isOccluded to prevent errant AOD->OCCLUDED")
                     }
                 }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index cd28bec..8f50b03 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
@@ -59,7 +59,7 @@
     private val communalInteractor: CommunalInteractor,
     private val communalSceneInteractor: CommunalSceneInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
-    val deviceEntryRepository: DeviceEntryRepository,
+    val deviceEntryInteractor: DeviceEntryInteractor,
     private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor,
     private val dreamManager: DreamManager,
 ) :
@@ -146,7 +146,7 @@
                         isIdleOnCommunal,
                         canTransitionToGoneOnWake,
                         primaryBouncerShowing) ->
-                    if (!deviceEntryRepository.isLockscreenEnabled()) {
+                    if (!deviceEntryInteractor.isLockscreenEnabled()) {
                         if (SceneContainerFlag.isEnabled) {
                             // TODO(b/336576536): Check if adaptation for scene framework is needed
                         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index ec03a6d..046e79c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -188,7 +188,7 @@
      * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
      * but not vice-versa.
      */
-    val isDreaming: Flow<Boolean> = repository.isDreaming
+    val isDreaming: StateFlow<Boolean> = repository.isDreaming
 
     /** Whether the system is dreaming with an overlay active */
     val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
@@ -205,7 +205,8 @@
                         trySendWithFailureLogging(
                             cameraLaunchSourceIntToModel(source),
                             TAG,
-                            "updated onCameraLaunchGestureDetected")
+                            "updated onCameraLaunchGestureDetected"
+                        )
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 7a06d2f..cd49c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -129,13 +129,16 @@
         }
     }
 
-    /** Notifies that the user has long-pressed on the lock screen. */
-    fun onLongPress() {
+    /** Notifies that the user has long-pressed on the lock screen.
+     *
+     * @param isA11yAction: Whether the action was performed as an a11y action
+     */
+    fun onLongPress(isA11yAction: Boolean = false) {
         if (!isLongPressHandlingEnabled.value) {
             return
         }
 
-        if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) {
+        if (isA11yAction) {
             showSettings()
         } else {
             showMenu()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 76f7749..1b9788f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -74,8 +74,8 @@
         val bgView = view.bgView
         longPressHandlingView.listener =
             object : LongPressHandlingView.Listener {
-                override fun onLongPressDetected(view: View, x: Int, y: Int) {
-                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+                    if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
                         return
                     }
                     vibratorHelper.performHapticFeedback(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
index 057b4f9..b387855 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
 import com.android.systemui.common.ui.view.LongPressHandlingView
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
 import com.android.systemui.plugins.FalsingManager
 
 object KeyguardLongPressViewBinder {
@@ -43,14 +46,19 @@
         onSingleTap: () -> Unit,
         falsingManager: FalsingManager,
     ) {
+        view.accessibilityHintLongPressAction =
+            AccessibilityNodeInfo.AccessibilityAction(
+                AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+                view.resources.getString(R.string.lock_screen_settings)
+            )
         view.listener =
             object : LongPressHandlingView.Listener {
-                override fun onLongPressDetected(view: View, x: Int, y: Int) {
-                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) {
+                    if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
                         return
                     }
 
-                    viewModel.onLongPress()
+                    viewModel.onLongPress(isA11yAction)
                 }
 
                 override fun onSingleTapDetected(view: View) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
index 940d1e1..0532ee2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -25,6 +25,8 @@
 import android.util.ArrayMap
 import android.util.Log
 import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.runBlocking
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -55,7 +57,14 @@
 
         var observer: PreviewLifecycleObserver? = null
         return try {
-            val renderer = previewRendererFactory.create(request)
+            val renderer =
+                if (Flags.lockscreenPreviewRendererCreateOnMainThread()) {
+                    runBlocking ("$TAG#previewRendererFactory.create", mainDispatcher) {
+                        previewRendererFactory.create(request)
+                    }
+                } else {
+                    previewRendererFactory.create(request)
+                }
 
             observer =
                 PreviewLifecycleObserver(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index bb4fb79..e9db1d2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -83,6 +83,7 @@
                     MathUtils.lerp(startAlpha, 0f, it)
                 }
             },
+            onFinish = { 1f },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index f1cbf25..1d2edc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,9 +33,12 @@
     /** Whether the long-press handling feature should be enabled. */
     val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
 
-    /** Notifies that the user has long-pressed on the lock screen. */
-    fun onLongPress() {
-        interactor.onLongPress()
+    /** Notifies that the user has long-pressed on the lock screen.
+     *
+     * @param isA11yAction: Whether the action was performed as an a11y action
+     */
+    fun onLongPress(isA11yAction: Boolean) {
+        interactor.onLongPress(isA11yAction)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 46ba5d1..8811908 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -80,6 +80,7 @@
                     1f - it
                 }
             },
+            onFinish = { 1f },
         )
 
     /** Bouncer container alpha */
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index d1c9b8e..b2ba0e1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -169,6 +169,14 @@
         return factory.create("NotifRemoteInputLog", 50 /* maxSize */, false /* systrace */);
     }
 
+    /** Provides a logging buffer for all logs related to notification visual stability. */
+    @Provides
+    @SysUISingleton
+    @VisualStabilityLog
+    public static LogBuffer provideVisualStabilityLogBuffer(LogBufferFactory factory) {
+        return factory.create("VisualStabilityLog", 50 /* maxSize */, false /* systrace */);
+    }
+
     /** Provides a logging buffer for all logs related to keyguard media controller. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt
new file mode 100644
index 0000000..b45ffc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for visual stability-related messages. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class VisualStabilityLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 46c5c18..c5d7b25 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -888,6 +888,8 @@
         heightInSceneContainerPx = height
         mediaCarouselScrollHandler.playerWidthPlusPadding =
             width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+        mediaContent.minimumWidth = widthInSceneContainerPx
+        mediaContent.minimumHeight = heightInSceneContainerPx
         updatePlayers(recreateMedia = true)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index f004c3a..6c53374 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -187,44 +187,17 @@
             }
         }
 
-        TextPaint paint = new TextPaint();
-        paint.setTextSize(42);
-
         CharSequence dialogText = null;
         CharSequence dialogTitle = null;
-        String appName = null;
-        if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) {
+
+        final String appName = extractAppName(aInfo, packageManager);
+        final boolean hasCastingCapabilities =
+                Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName);
+
+        if (hasCastingCapabilities) {
             dialogText = getString(R.string.media_projection_sys_service_dialog_warning);
             dialogTitle = getString(R.string.media_projection_sys_service_dialog_title);
         } else {
-            String label = aInfo.loadLabel(packageManager).toString();
-
-            // If the label contains new line characters it may push the security
-            // message below the fold of the dialog. Labels shouldn't have new line
-            // characters anyways, so just truncate the message the first time one
-            // is seen.
-            final int labelLength = label.length();
-            int offset = 0;
-            while (offset < labelLength) {
-                final int codePoint = label.codePointAt(offset);
-                final int type = Character.getType(codePoint);
-                if (type == Character.LINE_SEPARATOR
-                        || type == Character.CONTROL
-                        || type == Character.PARAGRAPH_SEPARATOR) {
-                    label = label.substring(0, offset) + ELLIPSIS;
-                    break;
-                }
-                offset += Character.charCount(codePoint);
-            }
-
-            if (label.isEmpty()) {
-                label = mPackageName;
-            }
-
-            String unsanitizedAppName = TextUtils.ellipsize(label,
-                    paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();
-            appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);
-
             String actionText = getString(R.string.media_projection_dialog_warning, appName);
             SpannableString message = new SpannableString(actionText);
 
@@ -255,6 +228,7 @@
                                 grantMediaProjectionPermission(selectedOption.getMode());
                             },
                             () -> finish(RECORD_CANCEL, /* projection= */ null),
+                            hasCastingCapabilities,
                             appName,
                             overrideDisableSingleAppOption,
                             mUid,
@@ -289,6 +263,47 @@
         }
     }
 
+    private String extractAppName(ApplicationInfo applicationInfo, PackageManager packageManager) {
+        String label = applicationInfo.loadLabel(packageManager).toString();
+
+        // If the label contains new line characters it may push the security
+        // message below the fold of the dialog. Labels shouldn't have new line
+        // characters anyways, so just truncate the message the first time one
+        // is seen.
+        final int labelLength = label.length();
+        int offset = 0;
+        while (offset < labelLength) {
+            final int codePoint = label.codePointAt(offset);
+            final int type = Character.getType(codePoint);
+            if (type == Character.LINE_SEPARATOR
+                    || type == Character.CONTROL
+                    || type == Character.PARAGRAPH_SEPARATOR) {
+                label = label.substring(0, offset) + ELLIPSIS;
+                break;
+            }
+            offset += Character.charCount(codePoint);
+        }
+
+        if (label.isEmpty()) {
+            label = mPackageName;
+        }
+
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(42);
+
+        String unsanitizedAppName = TextUtils.ellipsize(label,
+                paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();
+        String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);
+
+        // Have app name be the package name as a default fallback, if specific app name can't be
+        // extracted
+        if (appName == null || appName.isEmpty()) {
+            return mPackageName;
+        }
+
+        return appName;
+    }
+
     @Override
     protected void onDestroy() {
         super.onDestroy();
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
index 9ce8070..6d1a458 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
@@ -29,13 +29,20 @@
     mediaProjectionConfig: MediaProjectionConfig?,
     private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
     private val onCancelClicked: Runnable,
-    private val appName: String?,
+    private val hasCastingCapabilities: Boolean,
+    appName: String,
     forceShowPartialScreenshare: Boolean,
     hostUid: Int,
     mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
 ) :
     BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
-        createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare),
+        createOptionList(
+            context,
+            appName,
+            hasCastingCapabilities,
+            mediaProjectionConfig,
+            forceShowPartialScreenshare
+        ),
         appName,
         hostUid,
         mediaProjectionMetricsLogger
@@ -43,7 +50,7 @@
     override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
         super.onCreate(dialog, savedInstanceState)
         // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
-        if (appName == null) {
+        if (hasCastingCapabilities) {
             setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
             setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
         } else {
@@ -65,30 +72,29 @@
     companion object {
         private fun createOptionList(
             context: Context,
-            appName: String?,
+            appName: String,
+            hasCastingCapabilities: Boolean,
             mediaProjectionConfig: MediaProjectionConfig?,
             overrideDisableSingleAppOption: Boolean = false,
         ): List<ScreenShareOption> {
             val singleAppWarningText =
-                if (appName == null) {
+                if (hasCastingCapabilities) {
                     R.string.media_projection_entry_cast_permission_dialog_warning_single_app
                 } else {
                     R.string.media_projection_entry_app_permission_dialog_warning_single_app
                 }
             val entireScreenWarningText =
-                if (appName == null) {
+                if (hasCastingCapabilities) {
                     R.string.media_projection_entry_cast_permission_dialog_warning_entire_screen
                 } else {
                     R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
                 }
 
-            // The single app option should only be disabled if there is an app name provided,
-            // the client has setup a MediaProjection with
-            // MediaProjectionConfig#createConfigForDefaultDisplay, AND it hasn't been overridden by
-            // the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
+            // The single app option should only be disabled if the client has setup a
+            // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
+            // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
             val singleAppOptionDisabled =
-                appName != null &&
-                    !overrideDisableSingleAppOption &&
+                !overrideDisableSingleAppOption &&
                     mediaProjectionConfig?.regionToCapture ==
                         MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
index 1dbd500..c4abcd2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
@@ -54,6 +54,7 @@
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.inputmethod.Flags;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
@@ -285,8 +286,11 @@
 
         // Set up the context group of buttons
         mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
+        final int switcherResId = Flags.imeSwitcherRevamp()
+                ? com.android.internal.R.drawable.ic_ime_switcher_new
+                : R.drawable.ic_ime_switcher_default;
         final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher,
-                mLightContext, R.drawable.ic_ime_switcher_default);
+                mLightContext, switcherResId);
         final ContextualButton accessibilityButton =
                 new ContextualButton(R.id.accessibility_button, mLightContext,
                         R.drawable.ic_sysbar_accessibility_button);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
index 8af5665..ee709c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the notification avalanche suppression flag state. */
+/** Helper for reading or using the new QS UI flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NewQsUI {
     /** The aconfig flag name */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
new file mode 100644
index 0000000..664d496
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.flags
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the new QS UI in NPVC flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object QSComposeFragment {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 720120b..5ea8c21 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -14,6 +14,8 @@
 
 package com.android.systemui.qs.tileimpl;
 
+import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ArgbEvaluator;
@@ -204,6 +206,9 @@
             values.setEvaluator(ArgbEvaluator.getInstance());
             mColorAnimator.setValues(values);
             mColorAnimator.removeAllListeners();
+            if (removeUpdateListenerInQsIconViewImpl()) {
+                mColorAnimator.removeAllUpdateListeners();
+            }
             mColorAnimator.addUpdateListener(animation -> {
                 setTint(iv, (int) animation.getAnimatedValue());
             });
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index b91891c..a300031 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 
 class ModesTile
 @Inject
@@ -91,8 +92,8 @@
 
     override fun newTileState() = BooleanState()
 
-    override fun handleClick(expandable: Expandable?) {
-        // TODO(b/346519570) open dialog
+    override fun handleClick(expandable: Expandable?) = runBlocking {
+        userActionInteractor.handleClick(expandable)
     }
 
     override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent
@@ -107,6 +108,7 @@
                 label = tileLabel
                 secondaryLabel = tileState.secondaryLabel
                 contentDescription = tileState.contentDescription
+                forceExpandIcon = true
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
index fd1f3d8..4c6563d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -16,19 +16,31 @@
 
 package com.android.systemui.qs.tiles.impl.modes.domain.interactor
 
+//noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click
 import android.content.Intent
 import android.provider.Settings
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
 import com.android.systemui.qs.tiles.base.interactor.QSTileInput
 import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
 import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
 
 class ModesTileUserActionInteractor
 @Inject
 constructor(
+    @Main private val coroutineContext: CoroutineContext,
     private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val dialogDelegate: ModesDialogDelegate,
 ) : QSTileUserActionInteractor<ModesTileModel> {
     val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
 
@@ -36,7 +48,7 @@
         with(input) {
             when (action) {
                 is QSTileUserAction.Click -> {
-                    // TODO(b/346519570) open dialog
+                    handleClick(action.expandable)
                 }
                 is QSTileUserAction.LongClick -> {
                     qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
@@ -44,4 +56,24 @@
             }
         }
     }
+
+    suspend fun handleClick(expandable: Expandable?) {
+        // Show a dialog with the list of modes to configure. Dialogs shown by the
+        // DialogTransitionAnimator must be created and shown on the main thread, so we post it to
+        // the UI handler.
+        withContext(coroutineContext) {
+            val dialog = dialogDelegate.createDialog()
+
+            expandable
+                ?.dialogTransitionController(
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
+                )
+                ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) }
+                ?: dialog.show()
+        }
+    }
+
+    companion object {
+        private const val INTERACTION_JANK_TAG = "configure_priority_modes"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 26b9a4c..7048ada 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -59,5 +59,6 @@
                     QSTileState.UserAction.CLICK,
                     QSTileState.UserAction.LONG_CLICK,
                 )
+            sideViewIcon = QSTileState.SideViewIcon.Chevron
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 8711e88..72f37fc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -149,6 +149,7 @@
             resetShadeSessions()
             handleKeyguardEnabledness()
             notifyKeyguardDismissCallbacks()
+            refreshLockscreenEnabled()
         } else {
             sceneLogger.logFrameworkEnabled(
                 isEnabled = false,
@@ -186,7 +187,6 @@
         applicationScope.launch {
             // TODO(b/296114544): Combine with some global hun state to make it visible!
             deviceProvisioningInteractor.isDeviceProvisioned
-                .distinctUntilChanged()
                 .flatMapLatest { isAllowedToBeVisible ->
                     if (isAllowedToBeVisible) {
                         combine(
@@ -735,4 +735,22 @@
             }
         }
     }
+
+    /**
+     * Keeps the value of [DeviceEntryInteractor.isLockscreenEnabled] fresh.
+     *
+     * This is needed because that value is sourced from a non-observable data source
+     * (`LockPatternUtils`, which doesn't expose a listener or callback for this value). Therefore,
+     * every time a transition to the `Lockscreen` scene is started, the value is re-fetched and
+     * cached.
+     */
+    private fun refreshLockscreenEnabled() {
+        applicationScope.launch {
+            sceneInteractor.transitionState
+                .map { it.isTransitioning(to = Scenes.Lockscreen) }
+                .distinctUntilChanged()
+                .filter { it }
+                .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
index 4f27b9e..b3d5c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
@@ -99,7 +99,7 @@
                         original,
                         updates.component,
                         updates.owner,
-                        type.displayId
+                        type.displayId,
                     )
             }
         return updated
@@ -120,6 +120,7 @@
         return replaceWithScreenshot(
             original = original,
             componentName = topMainRootTask?.topActivity ?: defaultComponent,
+            taskId = topMainRootTask?.taskId,
             owner = defaultOwner,
             displayId = original.displayId
         )
@@ -144,11 +145,12 @@
         )
     }
 
-    suspend fun replaceWithScreenshot(
+    private suspend fun replaceWithScreenshot(
         original: ScreenshotData,
         componentName: ComponentName?,
         owner: UserHandle?,
         displayId: Int,
+        taskId: Int? = null,
     ): ScreenshotData {
         Log.i(TAG, "Capturing screenshot: $componentName / $owner")
         val screenshot = captureDisplay(displayId)
@@ -157,7 +159,8 @@
             bitmap = screenshot,
             userHandle = owner,
             topComponent = componentName,
-            screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0)
+            screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0),
+            taskId = taskId ?: -1,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index d870fe6..b468d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -50,19 +50,21 @@
 import com.android.systemui.communal.util.CommunalColors
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
-import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.collectFlow
 import java.util.function.Consumer
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 /**
@@ -77,6 +79,7 @@
     private val communalInteractor: CommunalInteractor,
     private val communalViewModel: CommunalViewModel,
     private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val shadeInteractor: ShadeInteractor,
     private val powerManager: PowerManager,
     private val communalColors: CommunalColors,
@@ -149,6 +152,19 @@
     private var hubShowing = false
 
     /**
+     * True if we're transitioning to or from edit mode
+     *
+     * We block all touches and gestures when edit mode is open to prevent funky transition issues
+     * when entering and exiting edit mode because we delay exiting the hub scene when entering edit
+     * mode and enter the hub scene early when exiting edit mode to make for a smoother transition.
+     * Gestures during these transitions can result in broken and unexpected UI states.
+     *
+     * Tracks [CommunalInteractor.editActivityShowing] and the [KeyguardState.GONE] to
+     * [KeyguardState.GLANCEABLE_HUB] transition.
+     */
+    private var inEditModeTransition = false
+
+    /**
      * True if either the primary or alternate bouncer are open, meaning the hub should not receive
      * any touch input.
      */
@@ -165,7 +181,14 @@
      *
      * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
      */
-    private var shadeShowing = false
+    private var shadeShowingAndConsumingTouches = false
+
+    /**
+     * True if the shade ever fully expands and the user isn't interacting with it (aka finger on
+     * screen dragging). In this case, the shade should handle all touch events until it has fully
+     * collapsed.
+     */
+    private var userNotInteractiveAtShadeFullyExpanded = false
 
     /**
      * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes
@@ -317,9 +340,41 @@
         )
         collectFlow(
             containerView,
-            allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+            // When leaving edit mode, editActivityShowing is true until the edit mode activity
+            // finishes itself and the device locks, after which isInTransition will be true until
+            // we're fully on the hub.
+            anyOf(
+                communalInteractor.editActivityShowing,
+                keyguardTransitionInteractor.isInTransition(
+                    Edge.create(KeyguardState.GONE, KeyguardState.GLANCEABLE_HUB)
+                )
+            ),
             {
-                shadeShowing = it
+                inEditModeTransition = it
+                updateTouchHandlingState()
+            }
+        )
+        collectFlow(
+            containerView,
+            combine(
+                shadeInteractor.isAnyFullyExpanded,
+                shadeInteractor.isUserInteracting,
+                shadeInteractor.isShadeFullyCollapsed,
+                ::Triple
+            ),
+            { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) ->
+                val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting
+
+                // If we ever are fully expanded and not interacting, capture this state as we
+                // should not handle touches until we fully collapse again
+                userNotInteractiveAtShadeFullyExpanded =
+                    !isShadeFullyCollapsed &&
+                        (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive)
+
+                // If the shade reaches full expansion without interaction, then we should allow it
+                // to consume touches rather than handling it here until it disappears.
+                shadeShowingAndConsumingTouches =
+                    userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive
                 updateTouchHandlingState()
             }
         )
@@ -337,7 +392,11 @@
      * Also clears gesture exclusion zones when the hub is occluded or gone.
      */
     private fun updateTouchHandlingState() {
-        val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing)
+        // Only listen to gestures when we're settled in the hub keyguard state and the shade
+        // bouncer are not showing on top.
+        val shouldInterceptGestures =
+            hubShowing &&
+                !(shadeShowingAndConsumingTouches || anyBouncerShowing || inEditModeTransition)
         if (shouldInterceptGestures) {
             lifecycleRegistry.currentState = Lifecycle.State.RESUMED
         } else {
@@ -389,17 +448,18 @@
             return false
         }
 
-        return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false
+        return communalContainerView?.let { handleTouchEventOnCommunalView(ev) } ?: false
     }
 
-    private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
+    private fun handleTouchEventOnCommunalView(ev: MotionEvent): Boolean {
         val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
         val isUp = ev.actionMasked == MotionEvent.ACTION_UP
+        val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE
         val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
 
-        val hubOccluded = anyBouncerShowing || shadeShowing
+        val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches
 
-        if (isDown && !hubOccluded) {
+        if ((isDown || isMove) && !hubOccluded) {
             isTrackingHubTouch = true
         }
 
@@ -407,7 +467,7 @@
             if (isUp || isCancel) {
                 isTrackingHubTouch = false
             }
-            return dispatchTouchEvent(view, ev)
+            return dispatchTouchEvent(ev)
         }
 
         return false
@@ -417,7 +477,14 @@
      * Dispatches the touch event to the communal container and sends a user activity event to reset
      * the screen timeout.
      */
-    private fun dispatchTouchEvent(view: View, ev: MotionEvent): Boolean {
+    private fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        if (inEditModeTransition) {
+            // Consume but ignore touches while we're transitioning to or from edit mode so that the
+            // user can't trigger another transition, such as by swiping the hub away, tapping a
+            // widget, or opening the shade/bouncer. Doing any of these while transitioning can
+            // result in broken states.
+            return true
+        }
         try {
             var handled = false
             communalContainerWrapper?.dispatchTouchEvent(ev) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 4f6a64f..bd08685 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1265,20 +1265,20 @@
             mTranslationForFullShadeTransition = qsTranslation;
             updateQsFrameTranslation();
             float currentTranslation = mQsFrame.getTranslationY();
-            int clipTop = mEnableClipping
-                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
-            int clipBottom = mEnableClipping
-                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
+            int clipTop = (int) (top - currentTranslation - mQsFrame.getTop());
+            int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop());
             mVisible = qsVisible;
             mQs.setQsVisible(qsVisible);
-            mQs.setFancyClipping(
-                    mDisplayLeftInset,
-                    clipTop,
-                    mDisplayRightInset,
-                    clipBottom,
-                    radius,
-                    qsVisible && !mSplitShadeEnabled,
-                    mIsFullWidth);
+            if (mEnableClipping) {
+                mQs.setFancyClipping(
+                        mDisplayLeftInset,
+                        clipTop,
+                        mDisplayRightInset,
+                        clipBottom,
+                        radius,
+                        qsVisible && !mSplitShadeEnabled,
+                        mIsFullWidth);
+            }
 
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 5065baa..23e2620 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -76,7 +76,7 @@
         scope.launch {
             shadeInteractor.isAnyExpanded.collect {
                 if (!it) {
-                    runPostCollapseActions()
+                    withContext(mainDispatcher) { runPostCollapseActions() }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index c912616..b9d24ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -51,6 +51,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.KeyEvent;
@@ -200,6 +201,7 @@
      * event.
      */
     private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;
+    private final Context mContext;
     private final DisplayTracker mDisplayTracker;
     private final @Nullable CommandRegistry mRegistry;
     private final @Nullable DumpHandler mDumpHandler;
@@ -571,6 +573,7 @@
             DumpHandler dumpHandler,
             Lazy<PowerInteractor> powerInteractor
     ) {
+        mContext = context;
         mDisplayTracker = displayTracker;
         mRegistry = registry;
         mDumpHandler = dumpHandler;
@@ -1209,7 +1212,12 @@
             boolean showImeSwitcher) {
         if (displayId == INVALID_DISPLAY) return;
 
-        if (mLastUpdatedImeDisplayId != displayId
+        boolean isConcurrentMultiUserModeEnabled = UserManager.isVisibleBackgroundUsersEnabled()
+                && mContext.getResources().getBoolean(android.R.bool.config_perDisplayFocusEnabled)
+                && android.view.inputmethod.Flags.concurrentInputMethods();
+
+        if (!isConcurrentMultiUserModeEnabled
+                && mLastUpdatedImeDisplayId != displayId
                 && mLastUpdatedImeDisplayId != INVALID_DISPLAY) {
             // Set previous NavBar's IME window status as invisible when IME
             // window switched to another display for single-session IME case.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index 11ccdff..59fd0ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -57,7 +57,7 @@
         interactor.ongoingCallState
             .map { state ->
                 when (state) {
-                    is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden
+                    is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden()
                     is OngoingCallModel.InCall -> {
                         // This block mimics OngoingCallController#updateChip.
                         if (state.startTimeMs <= 0L) {
@@ -82,7 +82,7 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
 
     private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
         if (state.intent == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index bafec38..6ea72b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -44,9 +44,10 @@
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
-            setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
-                stopAction.invoke()
-            }
+            setPositiveButton(
+                R.string.cast_to_other_device_stop_dialog_button,
+                endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
index 7dc9b25..b0c8321 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
@@ -55,9 +55,10 @@
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
-            setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
-                stopAction.invoke()
-            }
+            setPositiveButton(
+                R.string.cast_to_other_device_stop_dialog_button,
+                endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index afa9cce..d9b0504 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -18,6 +18,9 @@
 
 import android.content.Context
 import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +38,7 @@
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.util.time.SystemClock
@@ -60,6 +64,7 @@
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val mediaRouterChipInteractor: MediaRouterChipInteractor,
     private val systemClock: SystemClock,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) : OngoingActivityChipViewModel {
@@ -74,18 +79,18 @@
         mediaProjectionChipInteractor.projection
             .map { projectionModel ->
                 when (projectionModel) {
-                    is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden
+                    is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
                     is ProjectionChipModel.Projecting -> {
                         if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
-                            OngoingActivityChipModel.Hidden
+                            OngoingActivityChipModel.Hidden()
                         } else {
                             createCastScreenToOtherDeviceChip(projectionModel)
                         }
                     }
                 }
             }
-            // See b/347726238.
-            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+            // See b/347726238 for [SharingStarted.Lazily] reasoning.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
 
     /**
      * The cast chip to show, based only on MediaRouter API events.
@@ -109,7 +114,7 @@
         mediaRouterChipInteractor.mediaRouterCastingState
             .map { routerModel ->
                 when (routerModel) {
-                    is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden
+                    is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden()
                     is MediaRouterCastModel.Casting -> {
                         // A consequence of b/269975671 is that MediaRouter will mark a device as
                         // casting before casting has actually started. To alleviate this bug a bit,
@@ -123,9 +128,9 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
 
-    override val chip: StateFlow<OngoingActivityChipModel> =
+    private val internalChip: StateFlow<OngoingActivityChipModel> =
         combine(projectionChip, routerChip) { projection, router ->
                 logger.log(
                     TAG,
@@ -159,17 +164,24 @@
                     router
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
+
+    private val hideChipDuringDialogTransitionHelper = ChipTransitionHelper(scope)
+
+    override val chip: StateFlow<OngoingActivityChipModel> =
+        hideChipDuringDialogTransitionHelper.createChipFlow(internalChip)
 
     /** Stops the currently active projection. */
-    private fun stopProjecting() {
-        logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (projection)" })
+    private fun stopProjectingFromDialog() {
+        logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (projection)" })
+        hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog()
         mediaProjectionChipInteractor.stopProjecting()
     }
 
     /** Stops the currently active media route. */
-    private fun stopMediaRouterCasting() {
-        logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (router)" })
+    private fun stopMediaRouterCastingFromDialog() {
+        logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (router)" })
+        hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog()
         mediaRouterChipInteractor.stopCasting()
     }
 
@@ -190,6 +202,8 @@
             startTimeMs = systemClock.elapsedRealtime(),
             createDialogLaunchOnClickListener(
                 createCastScreenToOtherDeviceDialogDelegate(state),
+                dialogTransitionAnimator,
+                DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Cast to other device"),
                 logger,
                 TAG,
             ),
@@ -207,6 +221,11 @@
             colors = ColorsModel.Red,
             createDialogLaunchOnClickListener(
                 createGenericCastToOtherDeviceDialogDelegate(deviceName),
+                dialogTransitionAnimator,
+                DialogCuj(
+                    Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+                    tag = "Cast to other device audio only",
+                ),
                 logger,
                 TAG,
             ),
@@ -219,7 +238,7 @@
         EndCastScreenToOtherDeviceDialogDelegate(
             endMediaProjectionDialogHelper,
             context,
-            stopAction = this::stopProjecting,
+            stopAction = this::stopProjectingFromDialog,
             state,
         )
 
@@ -228,7 +247,7 @@
             endMediaProjectionDialogHelper,
             context,
             deviceName,
-            stopAction = this::stopMediaRouterCasting,
+            stopAction = this::stopMediaRouterCastingFromDialog,
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
index 6004365..2d9ccb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
@@ -17,7 +17,9 @@
 package com.android.systemui.statusbar.chips.mediaprojection.ui.view
 
 import android.app.ActivityManager
+import android.content.DialogInterface
 import android.content.pm.PackageManager
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -29,6 +31,7 @@
 @Inject
 constructor(
     private val dialogFactory: SystemUIDialog.Factory,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val packageManager: PackageManager,
 ) {
     /** Creates a new [SystemUIDialog] using the given delegate. */
@@ -36,6 +39,28 @@
         return dialogFactory.create(delegate)
     }
 
+    /**
+     * Returns the click listener that should be invoked if a user clicks "Stop" on the end media
+     * projection dialog.
+     *
+     * The click listener will invoke [stopAction] and also do some UI manipulation.
+     *
+     * @param stopAction an action that, when invoked, should notify system API(s) that the media
+     *   projection should be stopped.
+     */
+    fun wrapStopAction(stopAction: () -> Unit): DialogInterface.OnClickListener {
+        return DialogInterface.OnClickListener { _, _ ->
+            // If the projection is stopped, then the chip will disappear, so we don't want the
+            // dialog to animate back into the chip just for the chip to disappear in a few frames.
+            dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+            stopAction.invoke()
+            // TODO(b/332662551): If the projection is stopped, there's a brief moment where the
+            // dialog closes and the chip re-shows because the system APIs haven't come back and
+            // told SysUI that the projection has officially stopped. It would be great for the chip
+            // to not re-show at all.
+        }
+    }
+
     fun getAppName(state: MediaProjectionState.Projecting): CharSequence? {
         val specificTaskInfo =
             if (state is MediaProjectionState.Projecting.SingleTask) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
index 1eca827..72656ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -52,9 +52,10 @@
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
-            setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ ->
-                stopAction.invoke()
-            }
+            setPositiveButton(
+                R.string.screenrecord_stop_dialog_button,
+                endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 0c34981..fcf3de4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -19,6 +19,9 @@
 import android.app.ActivityManager
 import android.content.Context
 import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -32,8 +35,10 @@
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
 import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.util.time.SystemClock
@@ -52,15 +57,18 @@
     @Application private val scope: CoroutineScope,
     private val context: Context,
     private val interactor: ScreenRecordChipInteractor,
+    private val shareToAppChipViewModel: ShareToAppChipViewModel,
     private val systemClock: SystemClock,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) : OngoingActivityChipViewModel {
-    override val chip: StateFlow<OngoingActivityChipModel> =
+
+    private val internalChip =
         interactor.screenRecordState
             .map { state ->
                 when (state) {
-                    is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
+                    is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden()
                     is ScreenRecordChipModel.Starting -> {
                         OngoingActivityChipModel.Shown.Countdown(
                             colors = ColorsModel.Red,
@@ -80,6 +88,11 @@
                             startTimeMs = systemClock.elapsedRealtime(),
                             createDialogLaunchOnClickListener(
                                 createDelegate(state.recordedTask),
+                                dialogTransitionAnimator,
+                                DialogCuj(
+                                    Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+                                    tag = "Screen record",
+                                ),
                                 logger,
                                 TAG,
                             ),
@@ -87,8 +100,13 @@
                     }
                 }
             }
-            // See b/347726238.
-            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+            // See b/347726238 for [SharingStarted.Lazily] reasoning.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
+
+    private val chipTransitionHelper = ChipTransitionHelper(scope)
+
+    override val chip: StateFlow<OngoingActivityChipModel> =
+        chipTransitionHelper.createChipFlow(internalChip)
 
     private fun createDelegate(
         recordedTask: ActivityManager.RunningTaskInfo?
@@ -96,13 +114,15 @@
         return EndScreenRecordingDialogDelegate(
             endMediaProjectionDialogHelper,
             context,
-            stopAction = this::stopRecording,
+            stopAction = this::stopRecordingFromDialog,
             recordedTask,
         )
     }
 
-    private fun stopRecording() {
-        logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested" })
+    private fun stopRecordingFromDialog() {
+        logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested from dialog" })
+        chipTransitionHelper.onActivityStoppedFromDialog()
+        shareToAppChipViewModel.onRecordingStoppedFromDialog()
         interactor.stopRecording()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
index 564f20e..d10bd77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
@@ -44,9 +44,10 @@
             // No custom on-click, because the dialog will automatically be dismissed when the
             // button is clicked anyway.
             setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
-            setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ ->
-                stopAction.invoke()
-            }
+            setPositiveButton(
+                R.string.share_to_app_stop_dialog_button,
+                endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index ddebd3a..85973fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -18,6 +18,9 @@
 
 import android.content.Context
 import androidx.annotation.DrawableRes
+import com.android.internal.jank.Cuj
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -32,6 +35,7 @@
 import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.util.time.SystemClock
@@ -55,28 +59,49 @@
     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
     private val systemClock: SystemClock,
     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) : OngoingActivityChipViewModel {
-    override val chip: StateFlow<OngoingActivityChipModel> =
+    private val internalChip =
         mediaProjectionChipInteractor.projection
             .map { projectionModel ->
                 when (projectionModel) {
-                    is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden
+                    is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
                     is ProjectionChipModel.Projecting -> {
                         if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) {
-                            OngoingActivityChipModel.Hidden
+                            OngoingActivityChipModel.Hidden()
                         } else {
                             createShareToAppChip(projectionModel)
                         }
                     }
                 }
             }
-            // See b/347726238.
-            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+            // See b/347726238 for [SharingStarted.Lazily] reasoning.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
+
+    private val chipTransitionHelper = ChipTransitionHelper(scope)
+
+    override val chip: StateFlow<OngoingActivityChipModel> =
+        chipTransitionHelper.createChipFlow(internalChip)
+
+    /**
+     * Notifies this class that the user just stopped a screen recording from the dialog that's
+     * shown when you tap the recording chip.
+     */
+    fun onRecordingStoppedFromDialog() {
+        // When a screen recording is active, share-to-app is also active (screen recording is just
+        // a special case of share-to-app, where the specific app receiving the share is System UI).
+        // When a screen recording is stopped, we immediately hide the screen recording chip in
+        // [com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel].
+        // We *also* need to immediately hide the share-to-app chip so it doesn't briefly show.
+        // See b/350891338.
+        chipTransitionHelper.onActivityStoppedFromDialog()
+    }
 
     /** Stops the currently active projection. */
-    private fun stopProjecting() {
-        logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested" })
+    private fun stopProjectingFromDialog() {
+        logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" })
+        chipTransitionHelper.onActivityStoppedFromDialog()
         mediaProjectionChipInteractor.stopProjecting()
     }
 
@@ -92,7 +117,16 @@
             colors = ColorsModel.Red,
             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
             startTimeMs = systemClock.elapsedRealtime(),
-            createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state), logger, TAG),
+            createDialogLaunchOnClickListener(
+                createShareToAppDialogDelegate(state),
+                dialogTransitionAnimator,
+                DialogCuj(
+                    Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
+                    tag = "Share to app",
+                ),
+                logger,
+                TAG,
+            ),
         )
     }
 
@@ -100,7 +134,7 @@
         EndShareToAppDialogDelegate(
             endMediaProjectionDialogHelper,
             context,
-            stopAction = this::stopProjecting,
+            stopAction = this::stopProjectingFromDialog,
             state,
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
index b2140f7..130b117 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.res.ColorStateList
+import android.view.ContextThemeWrapper
 import androidx.annotation.ColorInt
 import com.android.settingslib.Utils
 import com.android.systemui.res.R
@@ -41,8 +42,11 @@
 
     /** The chip should have a red background with white text. */
     data object Red : ColorsModel {
-        override fun background(context: Context): ColorStateList =
-            ColorStateList.valueOf(context.getColor(R.color.GM2_red_600))
+        override fun background(context: Context): ColorStateList {
+            val themedContext =
+                ContextThemeWrapper(context, com.android.internal.R.style.Theme_DeviceDefault_Light)
+            return Utils.getColorAttr(themedContext, com.android.internal.R.attr.materialColorError)
+        }
 
         override fun text(context: Context) = context.getColor(android.R.color.white)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 40f86f9..17cf60b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -24,9 +24,15 @@
     /** Condensed name representing the model, used for logs. */
     abstract val logName: String
 
-    /** This chip shouldn't be shown. */
-    data object Hidden : OngoingActivityChipModel() {
-        override val logName = "Hidden"
+    /**
+     * This chip shouldn't be shown.
+     *
+     * @property shouldAnimate true if the transition from [Shown] to [Hidden] should be animated,
+     *   and false if that transition should *not* be animated (i.e. the chip view should
+     *   immediately disappear).
+     */
+    data class Hidden(val shouldAnimate: Boolean = true) : OngoingActivityChipModel() {
+        override val logName = "Hidden(anim=$shouldAnimate)"
     }
 
     /** This chip should be shown with the given information. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt
new file mode 100644
index 0000000..92e72c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.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.systemui.statusbar.chips.ui.viewmodel
+
+import android.annotation.SuppressLint
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.launch
+
+/**
+ * A class that can help [OngoingActivityChipViewModel] instances with various transition states.
+ *
+ * For now, this class's only functionality is immediately hiding the chip if the user has tapped an
+ * activity chip and then clicked "Stop" on the resulting dialog. There's a bit of a delay between
+ * when the user clicks "Stop" and when the system services notify SysUI that the activity has
+ * indeed stopped. We don't want the chip to briefly show for a few frames during that delay, so
+ * this class helps us immediately hide the chip as soon as the user clicks "Stop" in the dialog.
+ * See b/353249803#comment4.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class ChipTransitionHelper(@Application private val scope: CoroutineScope) {
+    /** A flow that emits each time the user has clicked "Stop" on the dialog. */
+    @SuppressLint("SharedFlowCreation")
+    private val activityStoppedFromDialogEvent = MutableSharedFlow<Unit>()
+
+    /** True if the user recently stopped the activity from the dialog. */
+    private val wasActivityRecentlyStoppedFromDialog: Flow<Boolean> =
+        activityStoppedFromDialogEvent
+            .transformLatest {
+                // Give system services 500ms to stop the activity and notify SysUI. Once more than
+                // 500ms has elapsed, we should go back to using the current system service
+                // information as the source of truth.
+                emit(true)
+                delay(500)
+                emit(false)
+            }
+            // Use stateIn so that the flow created in [createChipFlow] is guaranteed to
+            // emit. (`combine`s require that all input flows have emitted.)
+            .stateIn(scope, SharingStarted.Lazily, false)
+
+    /**
+     * Notifies this class that the user just clicked "Stop" on the stop dialog that's shown when
+     * the chip is tapped.
+     *
+     * Call this method in order to immediately hide the chip.
+     */
+    fun onActivityStoppedFromDialog() {
+        // Because this event causes UI changes, make sure it's launched on the main thread scope.
+        scope.launch { activityStoppedFromDialogEvent.emit(Unit) }
+    }
+
+    /**
+     * Creates a flow that will forcibly hide the chip if the user recently stopped the activity
+     * (see [onActivityStoppedFromDialog]). In general, this flow just uses value in [chip].
+     */
+    fun createChipFlow(chip: Flow<OngoingActivityChipModel>): StateFlow<OngoingActivityChipModel> {
+        return combine(
+                chip,
+                wasActivityRecentlyStoppedFromDialog,
+            ) { chipModel, activityRecentlyStopped ->
+                if (activityRecentlyStopped) {
+                    // There's a bit of a delay between when the user stops an activity via
+                    // SysUI and when the system services notify SysUI that the activity has
+                    // indeed stopped. Prevent the chip from showing during this delay by
+                    // immediately hiding it without any animation.
+                    OngoingActivityChipModel.Hidden(shouldAnimate = false)
+                } else {
+                    chipModel
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
index ee010f7..2fc366b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt
@@ -17,10 +17,14 @@
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
 import android.view.View
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.StatusBarChipsLog
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import kotlinx.coroutines.flow.StateFlow
 
@@ -36,13 +40,19 @@
         /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */
         fun createDialogLaunchOnClickListener(
             dialogDelegate: SystemUIDialog.Delegate,
+            dialogTransitionAnimator: DialogTransitionAnimator,
+            cuj: DialogCuj,
             @StatusBarChipsLog logger: LogBuffer,
             tag: String,
         ): View.OnClickListener {
-            return View.OnClickListener { _ ->
+            return View.OnClickListener { view ->
                 logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" })
                 val dialog = dialogDelegate.createDialog()
-                dialog.show()
+                val launchableView =
+                    view.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                dialogTransitionAnimator.showFromView(dialog, launchableView, cuj)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 15c348e..b0d897d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -26,11 +26,14 @@
 import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
 import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.util.kotlin.pairwise
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -50,49 +53,132 @@
     callChipViewModel: CallChipViewModel,
     @StatusBarChipsLog private val logger: LogBuffer,
 ) {
+    private enum class ChipType {
+        ScreenRecord,
+        ShareToApp,
+        CastToOtherDevice,
+        Call,
+    }
+
+    /** Model that helps us internally track the various chip states from each of the types. */
+    private sealed interface InternalChipModel {
+        /**
+         * Represents that we've internally decided to show the chip with type [type] with the given
+         * [model] information.
+         */
+        data class Shown(val type: ChipType, val model: OngoingActivityChipModel.Shown) :
+            InternalChipModel
+
+        /**
+         * Represents that all chip types would like to be hidden. Each value specifies *how* that
+         * chip type should get hidden.
+         */
+        data class Hidden(
+            val screenRecord: OngoingActivityChipModel.Hidden,
+            val shareToApp: OngoingActivityChipModel.Hidden,
+            val castToOtherDevice: OngoingActivityChipModel.Hidden,
+            val call: OngoingActivityChipModel.Hidden,
+        ) : InternalChipModel
+    }
+
+    private val internalChip: Flow<InternalChipModel> =
+        combine(
+            screenRecordChipViewModel.chip,
+            shareToAppChipViewModel.chip,
+            castToOtherDeviceChipViewModel.chip,
+            callChipViewModel.chip,
+        ) { screenRecord, shareToApp, castToOtherDevice, call ->
+            logger.log(
+                TAG,
+                LogLevel.INFO,
+                {
+                    str1 = screenRecord.logName
+                    str2 = shareToApp.logName
+                    str3 = castToOtherDevice.logName
+                },
+                { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
+            )
+            logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" })
+            // This `when` statement shows the priority order of the chips.
+            when {
+                // Screen recording also activates the media projection APIs, so whenever the
+                // screen recording chip is active, the media projection chip would also be
+                // active. We want the screen-recording-specific chip shown in this case, so we
+                // give the screen recording chip priority. See b/296461748.
+                screenRecord is OngoingActivityChipModel.Shown ->
+                    InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord)
+                shareToApp is OngoingActivityChipModel.Shown ->
+                    InternalChipModel.Shown(ChipType.ShareToApp, shareToApp)
+                castToOtherDevice is OngoingActivityChipModel.Shown ->
+                    InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice)
+                call is OngoingActivityChipModel.Shown ->
+                    InternalChipModel.Shown(ChipType.Call, call)
+                else -> {
+                    // We should only get here if all chip types are hidden
+                    check(screenRecord is OngoingActivityChipModel.Hidden)
+                    check(shareToApp is OngoingActivityChipModel.Hidden)
+                    check(castToOtherDevice is OngoingActivityChipModel.Hidden)
+                    check(call is OngoingActivityChipModel.Hidden)
+                    InternalChipModel.Hidden(
+                        screenRecord = screenRecord,
+                        shareToApp = shareToApp,
+                        castToOtherDevice = castToOtherDevice,
+                        call = call,
+                    )
+                }
+            }
+        }
+
     /**
      * A flow modeling the chip that should be shown in the status bar after accounting for possibly
-     * multiple ongoing activities.
+     * multiple ongoing activities and animation requirements.
      *
      * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for
      * actually displaying the chip.
      */
     val chip: StateFlow<OngoingActivityChipModel> =
-        combine(
-                screenRecordChipViewModel.chip,
-                shareToAppChipViewModel.chip,
-                castToOtherDeviceChipViewModel.chip,
-                callChipViewModel.chip,
-            ) { screenRecord, shareToApp, castToOtherDevice, call ->
-                logger.log(
-                    TAG,
-                    LogLevel.INFO,
-                    {
-                        str1 = screenRecord.logName
-                        str2 = shareToApp.logName
-                        str3 = castToOtherDevice.logName
-                    },
-                    { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." },
-                )
-                logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" })
-                // This `when` statement shows the priority order of the chips
-                when {
-                    // Screen recording also activates the media projection APIs, so whenever the
-                    // screen recording chip is active, the media projection chip would also be
-                    // active. We want the screen-recording-specific chip shown in this case, so we
-                    // give the screen recording chip priority. See b/296461748.
-                    screenRecord is OngoingActivityChipModel.Shown -> screenRecord
-                    shareToApp is OngoingActivityChipModel.Shown -> shareToApp
-                    castToOtherDevice is OngoingActivityChipModel.Shown -> castToOtherDevice
-                    else -> call
+        internalChip
+            .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL)
+            .map { (old, new) ->
+                if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) {
+                    // If we're transitioning from showing the chip to hiding the chip, different
+                    // chips require different animation behaviors. For example, the screen share
+                    // chips shouldn't animate if the user stopped the screen share from the dialog
+                    // (see b/353249803#comment4), but the call chip should always animate.
+                    //
+                    // This `when` block makes sure that when we're transitioning from Shown to
+                    // Hidden, we check what chip type was previously showing and we use that chip
+                    // type's hide animation behavior.
+                    when (old.type) {
+                        ChipType.ScreenRecord -> new.screenRecord
+                        ChipType.ShareToApp -> new.shareToApp
+                        ChipType.CastToOtherDevice -> new.castToOtherDevice
+                        ChipType.Call -> new.call
+                    }
+                } else if (new is InternalChipModel.Shown) {
+                    // If we have a chip to show, always show it.
+                    new.model
+                } else {
+                    // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we
+                    // choose because no animation should happen regardless.
+                    OngoingActivityChipModel.Hidden()
                 }
             }
             // Some of the chips could have timers in them and we don't want the start time
             // for those timers to get reset for any reason. So, as soon as any subscriber has
-            // requested the chip information, we need to maintain it forever. See b/347726238.
-            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
+            // requested the chip information, we maintain it forever by using
+            // [SharingStarted.Lazily]. See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden())
 
     companion object {
         private const val TAG = "ChipsViewModel"
+
+        private val DEFAULT_INTERNAL_HIDDEN_MODEL =
+            InternalChipModel.Hidden(
+                screenRecord = OngoingActivityChipModel.Hidden(),
+                shareToApp = OngoingActivityChipModel.Hidden(),
+                castToOtherDevice = OngoingActivityChipModel.Hidden(),
+                call = OngoingActivityChipModel.Hidden(),
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 3dcaff3..b342722 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -52,8 +52,11 @@
     }
 
     fun getNotificationBuckets(): IntArray {
-        if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled
-            || NotificationClassificationFlag.isEnabled) {
+        if (
+            PriorityPeopleSection.isEnabled ||
+                NotificationMinimalismPrototype.isEnabled ||
+                NotificationClassificationFlag.isEnabled
+        ) {
             // We don't need this list to be adaptive, it can be the superset of all features.
             return PriorityBucket.getAllInOrder()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index e48c28d..cb133ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -1005,6 +1005,16 @@
         mIsMarkedForUserTriggeredMovement = marked;
     }
 
+    private boolean mSeenInShade = false;
+
+    public void setSeenInShade(boolean seen) {
+        mSeenInShade = seen;
+    }
+
+    public boolean isSeenInShade() {
+        return mSeenInShade;
+    }
+
     public void setIsHeadsUpEntry(boolean isHeadsUpEntry) {
         mIsHeadsUpEntry = isHeadsUpEntry;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 55c6790..b1b2a65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -16,62 +16,15 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.app.NotificationManager
-import android.os.UserHandle
-import android.provider.Settings
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.Dumpable
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.expansionChanges
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.headsUpEvents
-import com.android.systemui.util.asIndenting
-import com.android.systemui.util.indentIfPossible
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
-import java.io.PrintWriter
 import javax.inject.Inject
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.yield
 
 /**
  * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
@@ -82,24 +35,10 @@
 class KeyguardCoordinator
 @Inject
 constructor(
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    private val dumpManager: DumpManager,
-    private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
-    private val keyguardRepository: KeyguardRepository,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    private val logger: KeyguardCoordinatorLogger,
-    @Application private val scope: CoroutineScope,
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider,
-    private val secureSettings: SecureSettings,
-    private val seenNotificationsInteractor: SeenNotificationsInteractor,
     private val statusBarStateController: StatusBarStateController,
-) : Coordinator, Dumpable {
-
-    private val unseenNotifications = mutableSetOf<NotificationEntry>()
-    private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
-    private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
-    private var unseenFilterEnabled = false
+) : Coordinator {
 
     override fun attach(pipeline: NotifPipeline) {
         setupInvalidateNotifListCallbacks()
@@ -107,385 +46,14 @@
         pipeline.addFinalizeFilter(notifFilter)
         keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter)
         updateSectionHeadersVisibility()
-        attachUnseenFilter(pipeline)
     }
 
-    private fun attachUnseenFilter(pipeline: NotifPipeline) {
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            pipeline.addPromoter(unseenNotifPromoter)
-            pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
-        }
-        pipeline.addFinalizeFilter(unseenNotifFilter)
-        pipeline.addCollectionListener(collectionListener)
-        scope.launch { trackUnseenFilterSettingChanges() }
-        dumpManager.registerDumpable(this)
-    }
-
-    private suspend fun trackSeenNotifications() {
-        // Whether or not keyguard is visible (or occluded).
-        val isKeyguardPresent: Flow<Boolean> =
-            keyguardTransitionInteractor
-                .transitionValue(
-                    scene = Scenes.Gone,
-                    stateWithoutSceneContainer = KeyguardState.GONE,
-                )
-                .map { it == 0f }
-                .distinctUntilChanged()
-                .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
-
-        // Separately track seen notifications while the device is locked, applying once the device
-        // is unlocked.
-        val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
-
-        // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
-        isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean ->
-            if (isKeyguardPresent) {
-                // Keyguard is not gone, notifications need to be visible for a certain threshold
-                // before being marked as seen
-                trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
-            } else {
-                // Mark all seen-while-locked notifications as seen for real.
-                if (notificationsSeenWhileLocked.isNotEmpty()) {
-                    unseenNotifications.removeAll(notificationsSeenWhileLocked)
-                    logger.logAllMarkedSeenOnUnlock(
-                        seenCount = notificationsSeenWhileLocked.size,
-                        remainingUnseenCount = unseenNotifications.size
-                    )
-                    notificationsSeenWhileLocked.clear()
-                }
-                unseenNotifFilter.invalidateList("keyguard no longer showing")
-                // Keyguard is gone, notifications can be immediately marked as seen when they
-                // become visible.
-                trackSeenNotificationsWhileUnlocked()
-            }
-        }
-    }
-
-    /**
-     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
-     * been "seen" while the device is on the keyguard.
-     */
-    private suspend fun trackSeenNotificationsWhileLocked(
-        notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
-    ) = coroutineScope {
-        // Remove removed notifications from the set
-        launch {
-            unseenEntryRemoved.collect { entry ->
-                if (notificationsSeenWhileLocked.remove(entry)) {
-                    logger.logRemoveSeenOnLockscreen(entry)
-                }
-            }
-        }
-        // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and
-        // is restarted when doze ends.
-        keyguardRepository.isDozing.collectLatest { isDozing ->
-            if (!isDozing) {
-                trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
-            }
-        }
-    }
-
-    /**
-     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
-     * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen
-     * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration.
-     */
-    private suspend fun trackSeenNotificationsWhileLockedAndNotDozing(
-        notificationsSeenWhileLocked: MutableSet<NotificationEntry>
-    ) = coroutineScope {
-        // All child tracking jobs will be cancelled automatically when this is cancelled.
-        val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>()
-
-        /**
-         * Wait for the user to spend enough time on the lock screen before removing notification
-         * from unseen set upon unlock.
-         */
-        suspend fun trackSeenDurationThreshold(entry: NotificationEntry) {
-            if (notificationsSeenWhileLocked.remove(entry)) {
-                logger.logResetSeenOnLockscreen(entry)
-            }
-            delay(SEEN_TIMEOUT)
-            notificationsSeenWhileLocked.add(entry)
-            trackingJobsByEntry.remove(entry)
-            logger.logSeenOnLockscreen(entry)
-        }
-
-        /** Stop any unseen tracking when a notification is removed. */
-        suspend fun stopTrackingRemovedNotifs(): Nothing =
-            unseenEntryRemoved.collect { entry ->
-                trackingJobsByEntry.remove(entry)?.let {
-                    it.cancel()
-                    logger.logStopTrackingLockscreenSeenDuration(entry)
-                }
-            }
-
-        /** Start tracking new notifications when they are posted. */
-        suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope {
-            unseenEntryAdded.collect { entry ->
-                logger.logTrackingLockscreenSeenDuration(entry)
-                // If this is an update, reset the tracking.
-                trackingJobsByEntry[entry]?.let {
-                    it.cancel()
-                    logger.logResetSeenOnLockscreen(entry)
-                }
-                trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
-            }
-        }
-
-        // Start tracking for all notifications that are currently unseen.
-        logger.logTrackingLockscreenSeenDuration(unseenNotifications)
-        unseenNotifications.forEach { entry ->
-            trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
-        }
-
-        launch { trackNewUnseenNotifs() }
-        launch { stopTrackingRemovedNotifs() }
-    }
-
-    // Track "seen" notifications, marking them as such when either shade is expanded or the
-    // notification becomes heads up.
-    private suspend fun trackSeenNotificationsWhileUnlocked() {
-        coroutineScope {
-            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
-            launch { markHeadsUpNotificationsAsSeen() }
-        }
-    }
-
-    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
-        statusBarStateController.expansionChanges.collectLatest { isExpanded ->
-            // Give keyguard events time to propagate, in case this expansion is part of the
-            // keyguard transition and not the user expanding the shade
-            yield()
-            if (isExpanded) {
-                logger.logShadeExpanded()
-                unseenNotifications.clear()
-            }
-        }
-    }
-
-    private suspend fun markHeadsUpNotificationsAsSeen() {
-        headsUpManager.allEntries
-            .filter { it.isRowPinned }
-            .forEach { unseenNotifications.remove(it) }
-        headsUpManager.headsUpEvents.collect { (entry, isHun) ->
-            if (isHun) {
-                logger.logUnseenHun(entry.key)
-                unseenNotifications.remove(entry)
-            }
-        }
-    }
-
-    private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
-        ) {
-            return flowOf(true)
-        }
-        return secureSettings
-            // emit whenever the setting has changed
-            .observerFlow(
-                UserHandle.USER_ALL,
-                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-            )
-            // perform a query immediately
-            .onStart { emit(Unit) }
-            // for each change, lookup the new value
-            .map {
-                secureSettings.getIntForUser(
-                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    def = 0,
-                    userHandle = UserHandle.USER_CURRENT,
-                ) == 1
-            }
-            // don't emit anything if nothing has changed
-            .distinctUntilChanged()
-            // perform lookups on the bg thread pool
-            .flowOn(bgDispatcher)
-            // only track the most recent emission, if events are happening faster than they can be
-            // consumed
-            .conflate()
-    }
-
-    private suspend fun trackUnseenFilterSettingChanges() {
-        unseenFeatureEnabled().collectLatest { setting ->
-            // update local field and invalidate if necessary
-            if (setting != unseenFilterEnabled) {
-                unseenFilterEnabled = setting
-                unseenNotifFilter.invalidateList("unseen setting changed")
-            }
-            // if the setting is enabled, then start tracking and filtering unseen notifications
-            if (setting) {
-                trackSeenNotifications()
-            }
-        }
-    }
-
-    private val collectionListener =
-        object : NotifCollectionListener {
-            override fun onEntryAdded(entry: NotificationEntry) {
-                if (
-                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
-                ) {
-                    logger.logUnseenAdded(entry.key)
-                    unseenNotifications.add(entry)
-                    unseenEntryAdded.tryEmit(entry)
-                }
-            }
-
-            override fun onEntryUpdated(entry: NotificationEntry) {
-                if (
-                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
-                ) {
-                    logger.logUnseenUpdated(entry.key)
-                    unseenNotifications.add(entry)
-                    unseenEntryAdded.tryEmit(entry)
-                }
-            }
-
-            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
-                if (unseenNotifications.remove(entry)) {
-                    logger.logUnseenRemoved(entry.key)
-                    unseenEntryRemoved.tryEmit(entry)
-                }
-            }
-        }
-
-    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
-        // Only ever elevate a top unseen notification on keyguard, not even locked shade
-        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
-            seenNotificationsInteractor.setTopOngoingNotification(null)
-            seenNotificationsInteractor.setTopUnseenNotification(null)
-            return
-        }
-        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
-        val nonSummaryEntries: Sequence<NotificationEntry> =
-            list
-                .asSequence()
-                .flatMap {
-                    when (it) {
-                        is NotificationEntry -> listOfNotNull(it)
-                        is GroupEntry -> it.children
-                        else -> error("unhandled type of $it")
-                    }
-                }
-                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
-        seenNotificationsInteractor.setTopOngoingNotification(
-            nonSummaryEntries
-                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
-                .minByOrNull { it.ranking.rank }
-        )
-        seenNotificationsInteractor.setTopUnseenNotification(
-            nonSummaryEntries
-                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
-                .minByOrNull { it.ranking.rank }
-        )
-    }
-
-    @VisibleForTesting
-    internal val unseenNotifPromoter =
-        object : NotifPromoter("$TAG-unseen") {
-            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
-                else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false
-                else
-                    seenNotificationsInteractor.isTopOngoingNotification(child) ||
-                        seenNotificationsInteractor.isTopUnseenNotification(child)
-        }
-
-    val topOngoingSectioner =
-        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
-                }
-            }
-        }
-
-    val topUnseenSectioner =
-        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
-                }
-            }
-        }
-
-    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
-        when {
-            predicate(representativeEntry) -> true
-            this !is GroupEntry -> false
-            else -> children.any(predicate)
-        }
-
-    @VisibleForTesting
-    internal val unseenNotifFilter =
-        object : NotifFilter("$TAG-unseen") {
-
-            var hasFilteredAnyNotifs = false
-
-            /**
-             * Encapsulates a definition of "being on the keyguard". Note that these two definitions
-             * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
-             * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
-             * is any state where the keyguard has not been dismissed, including locked shade and
-             * occluded lock screen.
-             *
-             * Returning false for locked shade and occluded states means that this filter will
-             * allow seen notifications to appear in the locked shade.
-             */
-            private fun isOnKeyguard(): Boolean =
-                if (NotificationMinimalismPrototype.V2.isEnabled) {
-                    false // disable this feature under this prototype
-                } else if (
-                    NotificationMinimalismPrototype.V1.isEnabled &&
-                        NotificationMinimalismPrototype.V1.showOnLockedShade
-                ) {
-                    statusBarStateController.state == StatusBarState.KEYGUARD
-                } else {
-                    keyguardRepository.isKeyguardShowing()
-                }
-
-            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
-                when {
-                    // Don't apply filter if the setting is disabled
-                    !unseenFilterEnabled -> false
-                    // Don't apply filter if the keyguard isn't currently showing
-                    !isOnKeyguard() -> false
-                    // Don't apply the filter if the notification is unseen
-                    unseenNotifications.contains(entry) -> false
-                    // Don't apply the filter to (non-promoted) group summaries
-                    //  - summary will be pruned if necessary, depending on if children are filtered
-                    entry.parent?.summary == entry -> false
-                    // Check that the entry satisfies certain characteristics that would bypass the
-                    // filter
-                    shouldIgnoreUnseenCheck(entry) -> false
-                    else -> true
-                }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered }
-
-            override fun onCleanup() {
-                logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs)
-                seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs)
-                hasFilteredAnyNotifs = false
-            }
-        }
-
     private val notifFilter: NotifFilter =
         object : NotifFilter(TAG) {
             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
                 keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
         }
 
-    private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean =
-        when {
-            entry.isMediaNotification -> true
-            entry.sbn.isOngoing -> true
-            else -> false
-        }
-
     // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on
     //  these same updates
     private fun setupInvalidateNotifListCallbacks() {}
@@ -502,22 +70,7 @@
         sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections
     }
 
-    override fun dump(pw: PrintWriter, args: Array<out String>) =
-        with(pw.asIndenting()) {
-            println(
-                "notificationListInteractor.hasFilteredOutSeenNotifications.value=" +
-                    seenNotificationsInteractor.hasFilteredOutSeenNotifications.value
-            )
-            println("unseen notifications:")
-            indentIfPossible {
-                for (notification in unseenNotifications) {
-                    println(notification.key)
-                }
-            }
-        }
-
     companion object {
         private const val TAG = "KeyguardCoordinator"
-        private val SEEN_TIMEOUT = 5.seconds
     }
 }
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
new file mode 100644
index 0000000..a6605f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+/**
+ * If the setting is enabled, this will track seen notifications and ensure that they only show in
+ * the shelf on the lockscreen.
+ *
+ * This class is a replacement of the [OriginalUnseenKeyguardCoordinator].
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class LockScreenMinimalismCoordinator
+@Inject
+constructor(
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val dumpManager: DumpManager,
+    private val headsUpInteractor: HeadsUpNotificationInteractor,
+    private val logger: LockScreenMinimalismCoordinatorLogger,
+    @Application private val scope: CoroutineScope,
+    private val secureSettings: SecureSettings,
+    private val seenNotificationsInteractor: SeenNotificationsInteractor,
+    private val statusBarStateController: StatusBarStateController,
+    private val shadeInteractor: ShadeInteractor,
+) : Coordinator, Dumpable {
+
+    private val unseenNotifications = mutableSetOf<NotificationEntry>()
+    private var isShadeVisible = false
+    private var unseenFilterEnabled = false
+
+    override fun attach(pipeline: NotifPipeline) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+            return
+        }
+        pipeline.addPromoter(unseenNotifPromoter)
+        pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+        pipeline.addCollectionListener(collectionListener)
+        scope.launch { trackUnseenFilterSettingChanges() }
+        dumpManager.registerDumpable(this)
+    }
+
+    private suspend fun trackSeenNotifications() {
+        coroutineScope {
+            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+            launch { markHeadsUpNotificationsAsSeen() }
+        }
+    }
+
+    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+        shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded ->
+            // Give keyguard events time to propagate, in case this expansion is part of the
+            // keyguard transition and not the user expanding the shade
+            delay(SHADE_VISIBLE_SEEN_TIMEOUT)
+            isShadeVisible = isExpanded
+            if (isExpanded) {
+                logger.logShadeVisible(unseenNotifications.size)
+                unseenNotifications.clear()
+                // no need to invalidateList; filtering is inactive while shade is open
+            } else {
+                logger.logShadeHidden()
+            }
+        }
+    }
+
+    private suspend fun markHeadsUpNotificationsAsSeen() {
+        headsUpInteractor.topHeadsUpRowIfPinned
+            .map { it?.let { headsUpInteractor.notificationKey(it) } }
+            .collectLatest { key ->
+                if (key == null) {
+                    logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false)
+                } else {
+                    val wasUnseenWhenPinned = unseenNotifications.any { it.key == key }
+                    logger.logTopHeadsUpRow(key, wasUnseenWhenPinned)
+                    if (wasUnseenWhenPinned) {
+                        delay(HEADS_UP_SEEN_TIMEOUT)
+                        val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key }
+                        logger.logHunHasBeenSeen(key, wasUnseenAfterDelay)
+                        // no need to invalidateList; nothing should change until after heads up
+                    }
+                }
+            }
+    }
+
+    private fun unseenFeatureEnabled(): Flow<Boolean> {
+        // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
+        //  Or should we actually just repurpose using the existing setting?
+        if (NotificationMinimalismPrototype.isEnabled) {
+            return flowOf(true)
+        }
+        return secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    def = 0,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
+    }
+
+    private suspend fun trackUnseenFilterSettingChanges() {
+        unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+            // update local field and invalidate if necessary
+            if (isSettingEnabled != unseenFilterEnabled) {
+                unseenFilterEnabled = isSettingEnabled
+                unseenNotifPromoter.invalidateList("unseen setting changed")
+            }
+            // if the setting is enabled, then start tracking and filtering unseen notifications
+            logger.logTrackingUnseen(isSettingEnabled)
+            if (isSettingEnabled) {
+                trackSeenNotifications()
+            }
+        }
+    }
+
+    private val collectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryAdded(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenAdded(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenUpdated(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                if (unseenNotifications.remove(entry)) {
+                    logger.logUnseenRemoved(entry.key)
+                }
+            }
+        }
+
+    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        // Only ever elevate a top unseen notification on keyguard, not even locked shade
+        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
+            seenNotificationsInteractor.setTopOngoingNotification(null)
+            seenNotificationsInteractor.setTopUnseenNotification(null)
+            return
+        }
+        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
+        val nonSummaryEntries: Sequence<NotificationEntry> =
+            list
+                .asSequence()
+                .flatMap {
+                    when (it) {
+                        is NotificationEntry -> listOfNotNull(it)
+                        is GroupEntry -> it.children
+                        else -> error("unhandled type of $it")
+                    }
+                }
+                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
+        seenNotificationsInteractor.setTopOngoingNotification(
+            nonSummaryEntries
+                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
+                .minByOrNull { it.ranking.rank }
+        )
+        seenNotificationsInteractor.setTopUnseenNotification(
+            nonSummaryEntries
+                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
+                .minByOrNull { it.ranking.rank }
+        )
+    }
+
+    @VisibleForTesting
+    val unseenNotifPromoter =
+        object : NotifPromoter(TAG) {
+            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
+                when {
+                    NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+                    seenNotificationsInteractor.isTopOngoingNotification(child) -> true
+                    !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+                    else -> seenNotificationsInteractor.isTopUnseenNotification(child)
+                }
+        }
+
+    val topOngoingSectioner =
+        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
+                }
+            }
+        }
+
+    val topUnseenSectioner =
+        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
+                }
+            }
+        }
+
+    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
+        when {
+            predicate(representativeEntry) -> true
+            this !is GroupEntry -> false
+            else -> children.any(predicate)
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) =
+        with(pw.asIndenting()) {
+            seenNotificationsInteractor.dump(this)
+            printCollection("unseen notifications", unseenNotifications) { println(it.key) }
+        }
+
+    companion object {
+        private const val TAG = "LockScreenMinimalismCoordinator"
+        private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds
+        private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
new file mode 100644
index 0000000..e44a77c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.UnseenNotificationLog
+import javax.inject.Inject
+
+private const val TAG = "LockScreenMinimalismCoordinator"
+
+class LockScreenMinimalismCoordinatorLogger
+@Inject
+constructor(
+    @UnseenNotificationLog private val buffer: LogBuffer,
+) {
+
+    fun logTrackingUnseen(trackingUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { bool1 = trackingUnseen },
+            messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+        )
+
+    fun logShadeVisible(numUnseen: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { int1 = numUnseen },
+            messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" }
+        )
+    }
+
+    fun logShadeHidden() {
+        buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.")
+    }
+
+    fun logUnseenAdded(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif added: $str1" },
+        )
+
+    fun logUnseenUpdated(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif updated: $str1" },
+        )
+
+    fun logUnseenRemoved(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif removed: $str1" },
+        )
+
+    fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseen
+            },
+            messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" },
+        )
+
+    fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseenWhenPinned
+            },
+            messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" },
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index e038982..73ce48b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -17,7 +17,11 @@
 
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
-import com.android.systemui.statusbar.notification.collection.*
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
+import com.android.systemui.statusbar.notification.collection.PipelineDumpable
+import com.android.systemui.statusbar.notification.collection.PipelineDumper
+import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
@@ -42,6 +46,8 @@
     hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
     hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
     keyguardCoordinator: KeyguardCoordinator,
+    unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+    lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
     rankingCoordinator: RankingCoordinator,
     colorizedFgsCoordinator: ColorizedFgsCoordinator,
     deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -82,6 +88,11 @@
         mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mCoordinators.add(lockScreenMinimalismCoordinator)
+        } else {
+            mCoordinators.add(unseenKeyguardCoordinator)
+        }
         mCoordinators.add(rankingCoordinator)
         mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
@@ -114,12 +125,12 @@
         }
 
         // Manually add Ordered Sections
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(keyguardCoordinator.topOngoingSectioner) // Top Ongoing
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
         }
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(keyguardCoordinator.topUnseenSectioner) // Top Unseen
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
         }
         mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
         if (PriorityPeopleSection.isEnabled) {
@@ -131,10 +142,10 @@
         }
         mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting
         if (NotificationClassificationFlag.isEnabled) {
-            mOrderedSections.add(bundleCoordinator.newsSectioner);
-            mOrderedSections.add(bundleCoordinator.socialSectioner);
-            mOrderedSections.add(bundleCoordinator.recsSectioner);
-            mOrderedSections.add(bundleCoordinator.promoSectioner);
+            mOrderedSections.add(bundleCoordinator.newsSectioner)
+            mOrderedSections.add(bundleCoordinator.socialSectioner)
+            mOrderedSections.add(bundleCoordinator.recsSectioner)
+            mOrderedSections.add(bundleCoordinator.promoSectioner)
         }
         mOrderedSections.add(rankingCoordinator.silentSectioner) // Silent
         mOrderedSections.add(rankingCoordinator.minimizedSectioner) // Minimized
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
new file mode 100644
index 0000000..5b25b11
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.expansionChanges
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.headsUpEvents
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.indentIfPossible
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.yield
+
+/**
+ * If the setting is enabled, this will track and hide seen notifications on the lockscreen.
+ *
+ * This is the "original" unseen keyguard coordinator because this is the logic originally developed
+ * for large screen devices where showing "seen" notifications on the lock screen was distracting.
+ * Moreover, this file was created during a project that will replace this logic, so the
+ * [LockScreenMinimalismCoordinator] is the expected replacement of this file.
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class OriginalUnseenKeyguardCoordinator
+@Inject
+constructor(
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val dumpManager: DumpManager,
+    private val headsUpManager: HeadsUpManager,
+    private val keyguardRepository: KeyguardRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val logger: KeyguardCoordinatorLogger,
+    @Application private val scope: CoroutineScope,
+    private val secureSettings: SecureSettings,
+    private val seenNotificationsInteractor: SeenNotificationsInteractor,
+    private val statusBarStateController: StatusBarStateController,
+) : Coordinator, Dumpable {
+
+    private val unseenNotifications = mutableSetOf<NotificationEntry>()
+    private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
+    private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1)
+    private var unseenFilterEnabled = false
+
+    override fun attach(pipeline: NotifPipeline) {
+        NotificationMinimalismPrototype.assertInLegacyMode()
+        pipeline.addFinalizeFilter(unseenNotifFilter)
+        pipeline.addCollectionListener(collectionListener)
+        scope.launch { trackUnseenFilterSettingChanges() }
+        dumpManager.registerDumpable(this)
+    }
+
+    private suspend fun trackSeenNotifications() {
+        // Whether or not keyguard is visible (or occluded).
+        @Suppress("DEPRECATION")
+        val isKeyguardPresentFlow: Flow<Boolean> =
+            keyguardTransitionInteractor
+                .transitionValue(
+                    scene = Scenes.Gone,
+                    stateWithoutSceneContainer = KeyguardState.GONE,
+                )
+                .map { it == 0f }
+                .distinctUntilChanged()
+                .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) }
+
+        // Separately track seen notifications while the device is locked, applying once the device
+        // is unlocked.
+        val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>()
+
+        // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes.
+        isKeyguardPresentFlow.collectLatest { isKeyguardPresent: Boolean ->
+            if (isKeyguardPresent) {
+                // Keyguard is not gone, notifications need to be visible for a certain threshold
+                // before being marked as seen
+                trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked)
+            } else {
+                // Mark all seen-while-locked notifications as seen for real.
+                if (notificationsSeenWhileLocked.isNotEmpty()) {
+                    unseenNotifications.removeAll(notificationsSeenWhileLocked)
+                    logger.logAllMarkedSeenOnUnlock(
+                        seenCount = notificationsSeenWhileLocked.size,
+                        remainingUnseenCount = unseenNotifications.size
+                    )
+                    notificationsSeenWhileLocked.clear()
+                }
+                unseenNotifFilter.invalidateList("keyguard no longer showing")
+                // Keyguard is gone, notifications can be immediately marked as seen when they
+                // become visible.
+                trackSeenNotificationsWhileUnlocked()
+            }
+        }
+    }
+
+    /**
+     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+     * been "seen" while the device is on the keyguard.
+     */
+    private suspend fun trackSeenNotificationsWhileLocked(
+        notificationsSeenWhileLocked: MutableSet<NotificationEntry>,
+    ) = coroutineScope {
+        // Remove removed notifications from the set
+        launch {
+            unseenEntryRemoved.collect { entry ->
+                if (notificationsSeenWhileLocked.remove(entry)) {
+                    logger.logRemoveSeenOnLockscreen(entry)
+                }
+            }
+        }
+        // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and
+        // is restarted when doze ends.
+        keyguardRepository.isDozing.collectLatest { isDozing ->
+            if (!isDozing) {
+                trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked)
+            }
+        }
+    }
+
+    /**
+     * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually
+     * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen
+     * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration.
+     */
+    private suspend fun trackSeenNotificationsWhileLockedAndNotDozing(
+        notificationsSeenWhileLocked: MutableSet<NotificationEntry>
+    ) = coroutineScope {
+        // All child tracking jobs will be cancelled automatically when this is cancelled.
+        val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>()
+
+        /**
+         * Wait for the user to spend enough time on the lock screen before removing notification
+         * from unseen set upon unlock.
+         */
+        suspend fun trackSeenDurationThreshold(entry: NotificationEntry) {
+            if (notificationsSeenWhileLocked.remove(entry)) {
+                logger.logResetSeenOnLockscreen(entry)
+            }
+            delay(SEEN_TIMEOUT)
+            notificationsSeenWhileLocked.add(entry)
+            trackingJobsByEntry.remove(entry)
+            logger.logSeenOnLockscreen(entry)
+        }
+
+        /** Stop any unseen tracking when a notification is removed. */
+        suspend fun stopTrackingRemovedNotifs(): Nothing =
+            unseenEntryRemoved.collect { entry ->
+                trackingJobsByEntry.remove(entry)?.let {
+                    it.cancel()
+                    logger.logStopTrackingLockscreenSeenDuration(entry)
+                }
+            }
+
+        /** Start tracking new notifications when they are posted. */
+        suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope {
+            unseenEntryAdded.collect { entry ->
+                logger.logTrackingLockscreenSeenDuration(entry)
+                // If this is an update, reset the tracking.
+                trackingJobsByEntry[entry]?.let {
+                    it.cancel()
+                    logger.logResetSeenOnLockscreen(entry)
+                }
+                trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+            }
+        }
+
+        // Start tracking for all notifications that are currently unseen.
+        logger.logTrackingLockscreenSeenDuration(unseenNotifications)
+        unseenNotifications.forEach { entry ->
+            trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) }
+        }
+
+        launch { trackNewUnseenNotifs() }
+        launch { stopTrackingRemovedNotifs() }
+    }
+
+    // Track "seen" notifications, marking them as such when either shade is expanded or the
+    // notification becomes heads up.
+    private suspend fun trackSeenNotificationsWhileUnlocked() {
+        coroutineScope {
+            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+            launch { markHeadsUpNotificationsAsSeen() }
+        }
+    }
+
+    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+        statusBarStateController.expansionChanges.collectLatest { isExpanded ->
+            // Give keyguard events time to propagate, in case this expansion is part of the
+            // keyguard transition and not the user expanding the shade
+            yield()
+            if (isExpanded) {
+                logger.logShadeExpanded()
+                unseenNotifications.clear()
+            }
+        }
+    }
+
+    private suspend fun markHeadsUpNotificationsAsSeen() {
+        headsUpManager.allEntries
+            .filter { it.isRowPinned }
+            .forEach { unseenNotifications.remove(it) }
+        headsUpManager.headsUpEvents.collect { (entry, isHun) ->
+            if (isHun) {
+                logger.logUnseenHun(entry.key)
+                unseenNotifications.remove(entry)
+            }
+        }
+    }
+
+    private fun unseenFeatureEnabled(): Flow<Boolean> {
+        if (NotificationMinimalismPrototype.isEnabled) {
+            // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
+            return flowOf(false)
+        }
+        return secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    def = 0,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
+    }
+
+    private suspend fun trackUnseenFilterSettingChanges() {
+        unseenFeatureEnabled().collectLatest { setting ->
+            // update local field and invalidate if necessary
+            if (setting != unseenFilterEnabled) {
+                unseenFilterEnabled = setting
+                unseenNotifFilter.invalidateList("unseen setting changed")
+            }
+            // if the setting is enabled, then start tracking and filtering unseen notifications
+            if (setting) {
+                trackSeenNotifications()
+            }
+        }
+    }
+
+    private val collectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryAdded(entry: NotificationEntry) {
+                if (
+                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+                ) {
+                    logger.logUnseenAdded(entry.key)
+                    unseenNotifications.add(entry)
+                    unseenEntryAdded.tryEmit(entry)
+                }
+            }
+
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                if (
+                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+                ) {
+                    logger.logUnseenUpdated(entry.key)
+                    unseenNotifications.add(entry)
+                    unseenEntryAdded.tryEmit(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                if (unseenNotifications.remove(entry)) {
+                    logger.logUnseenRemoved(entry.key)
+                    unseenEntryRemoved.tryEmit(entry)
+                }
+            }
+        }
+
+    @VisibleForTesting
+    val unseenNotifFilter =
+        object : NotifFilter(TAG) {
+
+            var hasFilteredAnyNotifs = false
+
+            override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
+                when {
+                    // Don't apply filter if the setting is disabled
+                    !unseenFilterEnabled -> false
+                    // Don't apply filter if the keyguard isn't currently showing
+                    !keyguardRepository.isKeyguardShowing() -> false
+                    // Don't apply the filter if the notification is unseen
+                    unseenNotifications.contains(entry) -> false
+                    // Don't apply the filter to (non-promoted) group summaries
+                    //  - summary will be pruned if necessary, depending on if children are filtered
+                    entry.parent?.summary == entry -> false
+                    // Check that the entry satisfies certain characteristics that would bypass the
+                    // filter
+                    shouldIgnoreUnseenCheck(entry) -> false
+                    else -> true
+                }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered }
+
+            override fun onCleanup() {
+                logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs)
+                seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs)
+                hasFilteredAnyNotifs = false
+            }
+        }
+
+    private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean =
+        when {
+            entry.isMediaNotification -> true
+            entry.sbn.isOngoing -> true
+            else -> false
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) =
+        with(pw.asIndenting()) {
+            println(
+                "notificationListInteractor.hasFilteredOutSeenNotifications.value=" +
+                    seenNotificationsInteractor.hasFilteredOutSeenNotifications.value
+            )
+            println("unseen notifications:")
+            indentIfPossible {
+                for (notification in unseenNotifications) {
+                    println(notification.key)
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "OriginalUnseenKeyguardCoordinator"
+        private val SEEN_TIMEOUT = 5.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index caa6c17..696298e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -18,8 +18,6 @@
 
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
 
-import android.util.Log;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
@@ -29,7 +27,10 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
@@ -41,7 +42,6 @@
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.util.Compile;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 
@@ -61,8 +61,6 @@
 // TODO(b/204468557): Move to @CoordinatorScope
 @SysUISingleton
 public class VisualStabilityCoordinator implements Coordinator, Dumpable {
-    public static final String TAG = "VisualStability";
-    public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private final DelayableExecutor mDelayableExecutor;
     private final HeadsUpManager mHeadsUpManager;
     private final SeenNotificationsInteractor mSeenNotificationsInteractor;
@@ -73,6 +71,8 @@
     private final VisualStabilityProvider mVisualStabilityProvider;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final CommunalInteractor mCommunalInteractor;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final VisualStabilityCoordinatorLogger mLogger;
 
     private boolean mSleepy = true;
     private boolean mFullyDozed;
@@ -81,6 +81,7 @@
     private boolean mNotifPanelCollapsing;
     private boolean mNotifPanelLaunchingActivity;
     private boolean mCommunalShowing = false;
+    private boolean mLockscreenShowing = false;
 
     private boolean mPipelineRunAllowed;
     private boolean mReorderingAllowed;
@@ -109,7 +110,9 @@
             VisibilityLocationProvider visibilityLocationProvider,
             VisualStabilityProvider visualStabilityProvider,
             WakefulnessLifecycle wakefulnessLifecycle,
-            CommunalInteractor communalInteractor) {
+            CommunalInteractor communalInteractor,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
+            VisualStabilityCoordinatorLogger logger) {
         mHeadsUpManager = headsUpManager;
         mShadeAnimationInteractor = shadeAnimationInteractor;
         mJavaAdapter = javaAdapter;
@@ -120,6 +123,8 @@
         mStatusBarStateController = statusBarStateController;
         mDelayableExecutor = delayableExecutor;
         mCommunalInteractor = communalInteractor;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+        mLogger = logger;
 
         dumpManager.registerDumpable(this);
     }
@@ -138,6 +143,9 @@
                 this::onLaunchingActivityChanged);
         mJavaAdapter.alwaysCollectFlow(mCommunalInteractor.isIdleOnCommunal(),
                 this::onCommunalShowingChanged);
+        mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.transitionValue(
+                        KeyguardState.LOCKSCREEN),
+                this::onLockscreenKeyguardStateTransitionValueChanged);
 
         pipeline.setVisualStabilityManager(mNotifStabilityManager);
     }
@@ -150,8 +158,9 @@
                     if (entry == null) {
                         return false;
                     }
-                    boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
-                            && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
+                    boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+                            && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
+                                || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
                     if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
                         return !mVisibilityLocationProvider.isInVisibleLocation(entry);
                     }
@@ -220,12 +229,12 @@
         boolean wasReorderingAllowed = mReorderingAllowed;
         mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity();
         mReorderingAllowed = isReorderingAllowed();
-        if (DEBUG && (wasPipelineRunAllowed != mPipelineRunAllowed
-                || wasReorderingAllowed != mReorderingAllowed)) {
-            Log.d(TAG, "Stability allowances changed:"
-                    + "  pipelineRunAllowed " + wasPipelineRunAllowed + "->" + mPipelineRunAllowed
-                    + "  reorderingAllowed " + wasReorderingAllowed + "->" + mReorderingAllowed
-                    + "  when setting " + field + "=" + value);
+        if (wasPipelineRunAllowed != mPipelineRunAllowed
+                || wasReorderingAllowed != mReorderingAllowed) {
+            mLogger.logAllowancesChanged(
+                    wasPipelineRunAllowed, mPipelineRunAllowed,
+                    wasReorderingAllowed, mReorderingAllowed,
+                    field, value);
         }
         if (mPipelineRunAllowed && mIsSuppressingPipelineRun) {
             mNotifStabilityManager.invalidateList("pipeline run suppression ended");
@@ -250,7 +259,9 @@
     }
 
     private boolean isReorderingAllowed() {
-        return ((mFullyDozed && mSleepy) || !mPanelExpanded || mCommunalShowing) && !mPulsing;
+        final boolean sleepyAndDozed = mFullyDozed && mSleepy;
+        final boolean stackShowing = mPanelExpanded || mLockscreenShowing;
+        return (sleepyAndDozed || !stackShowing || mCommunalShowing) && !mPulsing;
     }
 
     /**
@@ -363,4 +374,14 @@
         mCommunalShowing = isShowing;
         updateAllowedStates("communalShowing", isShowing);
     }
+
+    private void onLockscreenKeyguardStateTransitionValueChanged(float value) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            return;
+        }
+
+        final boolean isShowing = value > 0.0f;
+        mLockscreenShowing = isShowing;
+        updateAllowedStates("lockscreenShowing", isShowing);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt
new file mode 100644
index 0000000..fe23e4e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.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.statusbar.notification.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VisualStabilityLog
+import javax.inject.Inject
+
+private const val TAG = "VisualStability"
+
+class VisualStabilityCoordinatorLogger
+@Inject
+constructor(@VisualStabilityLog private val buffer: LogBuffer) {
+    fun logAllowancesChanged(
+        wasRunAllowed: Boolean,
+        isRunAllowed: Boolean,
+        wasReorderingAllowed: Boolean,
+        isReorderingAllowed: Boolean,
+        field: String,
+        value: Boolean
+    ) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = wasRunAllowed
+                bool2 = isRunAllowed
+                bool3 = wasReorderingAllowed
+                bool4 = isReorderingAllowed
+                str1 = field
+                str2 = value.toString()
+            },
+            {
+                "stability allowances changed:" +
+                    " pipelineRunAllowed $bool1->$bool2" +
+                    " reorderingAllowed $bool3->$bool4" +
+                    " when setting $str1=$str2"
+            }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
index 5adf31b..5614f3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt
@@ -2,6 +2,7 @@
 
 import android.util.ArraySet
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
 import com.android.systemui.util.ListenerSet
 import javax.inject.Inject
 
@@ -13,12 +14,18 @@
     /** The subset of active listeners which are temporary (will be removed after called) */
     private val temporaryListeners = ArraySet<OnReorderingAllowedListener>()
 
+    private val banListeners = ListenerSet<OnReorderingBannedListener>()
+
     var isReorderingAllowed = true
         set(value) {
             if (field != value) {
                 field = value
                 if (value) {
                     notifyReorderingAllowed()
+                } else if (NotificationThrottleHun.isEnabled){
+                    banListeners.forEach { listener ->
+                        listener.onReorderingBanned()
+                    }
                 }
             }
         }
@@ -38,6 +45,10 @@
         allListeners.addIfAbsent(listener)
     }
 
+    fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) {
+        banListeners.addIfAbsent(listener)
+    }
+
     /** Add a listener which will be removed when it is called. */
     fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) {
         // Only add to the temporary set if it was added to the global set
@@ -57,3 +68,7 @@
 fun interface OnReorderingAllowedListener {
     fun onReorderingAllowed()
 }
+
+fun interface OnReorderingBannedListener {
+    fun onReorderingBanned()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index bf44b9f..24b75d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -30,6 +30,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -44,8 +45,17 @@
     private val shadeInteractor: ShadeInteractor,
 ) {
 
+    /** The top-ranked heads up row, regardless of pinned state */
     val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
 
+    /** The top-ranked heads up row, if that row is pinned */
+    val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> =
+        headsUpRepository.topHeadsUpRow
+            .flatMapLatest { repository ->
+                repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null)
+            }
+            .distinctUntilChanged()
+
     /** Set of currently pinned top-level heads up rows to be displayed. */
     val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
@@ -89,10 +99,10 @@
             flowOf(false)
         } else {
             combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-                    hasPinnedRows,
-                    animatingAway ->
-                    hasPinnedRows || animatingAway
-                }
+                hasPinnedRows,
+                animatingAway ->
+                hasPinnedRows || animatingAway
+            }
         }
     }
 
@@ -127,6 +137,9 @@
 
     fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
 
+    /** Returns the Notification Key (the standard string) of this row. */
+    fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key
+
     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 85c66bd..948a3c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.util.IndentingPrintWriter
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.util.printSection
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
@@ -41,24 +43,42 @@
 
     /** Set the entry that is identified as the top ongoing notification. */
     fun setTopOngoingNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topOngoingNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top ongoing notification. */
     fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else
             entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
 
     /** Set the entry that is identified as the top unseen notification. */
     fun setTopUnseenNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topUnseenNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top unseen notification. */
     fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
+
+    fun dump(pw: IndentingPrintWriter) =
+        with(pw) {
+            printSection("SeenNotificationsInteractor") {
+                print(
+                    "hasFilteredOutSeenNotifications",
+                    notificationListRepository.hasFilteredOutSeenNotifications.value
+                )
+                print(
+                    "topOngoingNotificationKey",
+                    notificationListRepository.topOngoingNotificationKey.value
+                )
+                print(
+                    "topUnseenNotificationKey",
+                    notificationListRepository.topUnseenNotificationKey.value
+                )
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index b8af369..fe86375 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -122,12 +122,15 @@
                 val timeRemaining = parseTimeDelta(remaining)
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Paused(
                             timeRemaining = timeRemaining,
-                            resumeIntent = notification.findActionWithName("Resume"),
-                            resetIntent = notification.findActionWithName("Reset"),
+                            resumeIntent = notification.findStartIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -136,12 +139,15 @@
                 val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Running(
                             finishTime = finishTime,
-                            pauseIntent = notification.findActionWithName("Pause"),
-                            addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+                            pauseIntent = notification.findPauseIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -149,8 +155,34 @@
         }
     }
 
-    private fun Notification.findActionWithName(name: String): PendingIntent? {
-        return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+    private fun Notification.findPauseIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true }
+            ?.actionIntent
+    }
+
+    private fun Notification.findStartIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true }
+            ?.actionIntent
+    }
+
+    // TODO: b/352142761 - switch to system attributes for label and icon.
+    //   - We probably want a consistent look for the Reset button. (Double check with UX.)
+    //   - Using the custom assets now since I couldn't an existing "Reset" icon.
+    private fun Notification.findResetAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true
+        }
+    }
+
+    // TODO: b/352142761 - check with UX on whether this should be required.
+    //   - Alternative is to allow for optional actions in addition to main and reset.
+    //   - For optional actions, we should take the custom label and icon.
+    private fun Notification.findAddMinuteAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true
+        }
     }
 
     private fun parseCurrentTime(current: String): Long {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
index 5584701..33b2564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row.shared
 
+import android.app.Notification
 import android.app.PendingIntent
 import java.time.Duration
 
@@ -32,6 +33,9 @@
 ) : RichOngoingContentModel {
     /** The state (paused or running) of the timer, and relevant time */
     sealed interface TimerState {
+        val addMinuteAction: Notification.Action?
+        val resetAction: Notification.Action?
+
         /**
          * Indicates a running timer
          *
@@ -41,7 +45,8 @@
         data class Running(
             val finishTime: Long,
             val pauseIntent: PendingIntent?,
-            val addOneMinuteIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
 
         /**
@@ -53,7 +58,8 @@
         data class Paused(
             val timeRemaining: Duration,
             val resumeIntent: PendingIntent?,
-            val resetIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
index 0d83ace..8c95187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -18,8 +18,9 @@
 
 import android.annotation.DrawableRes
 import android.content.Context
+import android.graphics.BlendMode
 import android.util.AttributeSet
-import android.widget.Button
+import com.android.internal.widget.EmphasizedNotificationButton
 
 class TimerButtonView
 @JvmOverloads
@@ -28,14 +29,19 @@
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0,
-) : Button(context, attrs, defStyleAttr, defStyleRes) {
+) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) {
 
     private val Int.dp: Int
         get() = (this * context.resources.displayMetrics.density).toInt()
 
     fun setIcon(@DrawableRes icon: Int) {
         val drawable = context.getDrawable(icon)
+
+        drawable?.mutate()
+        drawable?.setTintList(textColors)
+        drawable?.setTintBlendMode(BlendMode.SRC_IN)
         drawable?.setBounds(0, 0, 24.dp, 24.dp)
+
         setCompoundDrawablesRelative(drawable, null, null, null)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
index 2e164d6..d481b50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification.row.ui.view
 
 import android.content.Context
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
 import android.os.SystemClock
 import android.util.AttributeSet
 import android.widget.Chronometer
@@ -48,6 +48,9 @@
     lateinit var altButton: TimerButtonView
         private set
 
+    lateinit var resetButton: TimerButtonView
+        private set
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         icon = requireViewById(R.id.icon)
@@ -56,13 +59,14 @@
         pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
         mainButton = requireViewById(R.id.mainButton)
         altButton = requireViewById(R.id.altButton)
+        resetButton = requireViewById(R.id.resetButton)
     }
 
     /** the resources configuration has changed such that the view needs to be reinflated */
     fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
 
-    fun setIcon(iconDrawable: Drawable?) {
-        this.icon.setImageDrawable(iconDrawable)
+    fun setIcon(icon: Icon?) {
+        this.icon.setImageIcon(icon)
     }
 
     fun setLabel(label: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
index c9ff589..042d1bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewbinder
 
+import android.content.res.ColorStateList
+import android.graphics.drawable.Icon
 import android.view.View
 import androidx.core.view.isGone
 import androidx.lifecycle.lifecycleScope
@@ -46,12 +48,43 @@
         launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
         launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
         launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+        launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } }
     }
 
     fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
         if (model != null) {
-            buttonView.setIcon(model.iconRes)
-            buttonView.setText(model.labelRes)
+            buttonView.setButtonBackground(
+                ColorStateList.valueOf(
+                    buttonView.context.getColor(com.android.internal.R.color.system_accent2_100)
+                )
+            )
+            buttonView.setTextColor(
+                buttonView.context.getColor(
+                    com.android.internal.R.color.notification_primary_text_color_light
+                )
+            )
+
+            when (model) {
+                is TimerViewModel.ButtonViewModel.WithSystemAttrs -> {
+                    buttonView.setIcon(model.iconRes)
+                    buttonView.setText(model.labelRes)
+                }
+                is TimerViewModel.ButtonViewModel.WithCustomAttrs -> {
+                    // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons
+                    // with empty resPackage? RemoteViews handles this by using a  different
+                    // `contextForResources` for inflation.
+                    val icon =
+                        if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "")
+                            Icon.createWithResource(
+                                "com.google.android.deskclock",
+                                model.icon.resId
+                            )
+                        else model.icon
+                    buttonView.setImageIcon(icon)
+                    buttonView.text = model.label
+                }
+            }
+
             buttonView.setOnClickListener(
                 model.pendingIntent?.let { pendingIntent ->
                     View.OnClickListener { pendingIntent.send() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
index a85c87f..768a093 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -19,7 +19,7 @@
 import android.annotation.DrawableRes
 import android.annotation.StringRes
 import android.app.PendingIntent
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
@@ -44,7 +44,7 @@
 
     private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
 
-    val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+    val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon }
 
     val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
 
@@ -57,13 +57,13 @@
         state.map {
             when (it) {
                 is TimerState.Paused ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.resumeIntent,
                         com.android.systemui.res.R.string.controls_media_resume, // "Resume",
                         com.android.systemui.res.R.drawable.ic_media_play
                     )
                 is TimerState.Running ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.pauseIntent,
                         com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
                         com.android.systemui.res.R.drawable.ic_media_pause
@@ -73,31 +73,41 @@
 
     val altButtonModel: Flow<ButtonViewModel?> =
         state.map {
-            when (it) {
-                is TimerState.Paused ->
-                    it.resetIntent?.let { resetIntent ->
-                        ButtonViewModel(
-                            resetIntent,
-                            com.android.systemui.res.R.string.reset, // "Reset",
-                            com.android.systemui.res.R.drawable.ic_close_white_rounded
-                        )
-                    }
-                is TimerState.Running ->
-                    it.addOneMinuteIntent?.let { addOneMinuteIntent ->
-                        ButtonViewModel(
-                            addOneMinuteIntent,
-                            com.android.systemui.res.R.string.add, // "Add 1 minute",
-                            com.android.systemui.res.R.drawable.ic_add
-                        )
-                    }
+            it.addMinuteAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "1:00",
+                    action.getIcon()
+                )
             }
         }
 
-    data class ButtonViewModel(
-        val pendingIntent: PendingIntent?,
-        @StringRes val labelRes: Int,
-        @DrawableRes val iconRes: Int,
-    )
+    val resetButtonModel: Flow<ButtonViewModel?> =
+        state.map {
+            it.resetAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "Reset",
+                    action.getIcon()
+                )
+            }
+        }
+
+    sealed interface ButtonViewModel {
+        val pendingIntent: PendingIntent?
+
+        data class WithSystemAttrs(
+            override val pendingIntent: PendingIntent?,
+            @StringRes val labelRes: Int,
+            @DrawableRes val iconRes: Int,
+        ) : ButtonViewModel
+
+        data class WithCustomAttrs(
+            override val pendingIntent: PendingIntent?,
+            val label: CharSequence,
+            val icon: Icon,
+        ) : ButtonViewModel
+    }
 }
 
 private fun Duration.format(): String {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
index bf37036..06f3db5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -24,102 +24,43 @@
 /** Helper for reading or using the minimalism prototype flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NotificationMinimalismPrototype {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
 
-    val version: Int by lazy {
-        SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
-    }
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
 
-    object V1 {
-        /** The aconfig flag name */
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+    /** Is the heads-up cycling animation enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationMinimalismPrototype()
 
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
+    /**
+     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
+     * not grouped, but this property read allows that behavior to be disabled.
+     */
+    val ungroupTopUnseen: Boolean
+        get() =
+            if (isUnexpectedlyInLegacyMode()) false
+            else
+                SystemProperties.getBoolean(
+                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
+                    false
+                )
 
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 1
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
 
-        /**
-         * the prototype will now show seen notifications on the locked shade by default, but this
-         * property read allows that to be quickly disabled for testing
-         */
-        val showOnLockedShade: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.show_on_locked_shade",
-                        true
-                    )
-
-        /** gets the configurable max number of notifications */
-        val maxNotifs: Int
-            get() =
-                if (isUnexpectedlyInLegacyMode()) -1
-                else
-                    SystemProperties.getInt(
-                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
-                        1
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
-    object V2 {
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
-
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 2
-
-        /**
-         * The prototype will (by default) use a promoter to ensure that the top unseen notification
-         * is not grouped, but this property read allows that behavior to be disabled.
-         */
-        val ungroupTopUnseen: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                        true
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index fbddc06..7c3072d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -66,6 +66,8 @@
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private float mStackTop;
     private float mStackCutoff;
+    private float mHeadsUpTop;
+    private float mHeadsUpBottom;
     private int mScrollY;
     private float mOverScrollTopAmount;
     private float mOverScrollBottomAmount;
@@ -377,6 +379,30 @@
         this.mStackCutoff = stackCutoff;
     }
 
+    /** y coordinate of the top position of a pinned HUN */
+    public float getHeadsUpTop() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        return mHeadsUpTop;
+    }
+
+    /** @see #getHeadsUpTop() */
+    public void setHeadsUpTop(float mHeadsUpTop) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        this.mHeadsUpTop = mHeadsUpTop;
+    }
+
+    /** the bottom-most y position where we can draw pinned HUNs  */
+    public float getHeadsUpBottom() {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+        return mHeadsUpBottom;
+    }
+
+    /** @see #getHeadsUpBottom() */
+    public void setHeadsUpBottom(float headsUpBottom) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        mHeadsUpBottom = headsUpBottom;
+    }
+
     public int getScrollY() {
         return mScrollY;
     }
@@ -784,7 +810,9 @@
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("mStackTop=" + mStackTop);
-        pw.println("mStackCutoff" + mStackCutoff);
+        pw.println("mStackCutoff=" + mStackCutoff);
+        pw.println("mHeadsUpTop=" + mHeadsUpTop);
+        pw.println("mHeadsUpBottom=" + mHeadsUpBottom);
         pw.println("mTopPadding=" + mTopPadding);
         pw.println("mStackTopMargin=" + mStackTopMargin);
         pw.println("mStackTranslation=" + mStackTranslation);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 2081adc..5d2b61b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -111,6 +111,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
@@ -834,7 +835,7 @@
             y = (int) mAmbientState.getStackCutoff();
             drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "getStackCutoff() = " + y);
 
-            y = (int) mScrollViewFields.getHeadsUpTop();
+            y = (int) mAmbientState.getHeadsUpTop();
             drawDebugInfo(canvas, y, Color.GREEN, /* label= */ "getHeadsUpTop() = " + y);
 
             y += getTopHeadsUpHeight();
@@ -1222,7 +1223,12 @@
 
     @Override
     public void setHeadsUpTop(float headsUpTop) {
-        mScrollViewFields.setHeadsUpTop(headsUpTop);
+        mAmbientState.setHeadsUpTop(headsUpTop);
+    }
+
+    @Override
+    public void setHeadsUpBottom(float headsUpBottom) {
+        mAmbientState.setHeadsUpBottom(headsUpBottom);
     }
 
     @Override
@@ -4857,14 +4863,23 @@
      * @param isHeadsUp true for appear, false for disappear animations
      */
     public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
-        final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
+        boolean addAnimation =
+                mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed);
+        if (NotificationThrottleHun.isEnabled()) {
+            final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null
+                    && row.getEntry().isSeenInShade();
+            addAnimation = addAnimation && !closedAndSeenInShade;
+        }
         if (SPEW) {
             Log.v(TAG, "generateHeadsUpAnimation:"
-                    + " willAdd=" + add
-                    + " isHeadsUp=" + isHeadsUp
-                    + " row=" + row.getEntry().getKey());
+                    + " addAnimation=" + addAnimation
+                    + (row.getEntry() == null ? " entry NULL "
+                            : " isSeenInShade=" + row.getEntry().isSeenInShade()
+                                    + " row=" + row.getEntry().getKey())
+                    + " mIsExpanded=" + mIsExpanded
+                    + " isHeadsUp=" + isHeadsUp);
         }
-        if (add) {
+        if (addAnimation) {
             // If we're hiding a HUN we just started showing THIS FRAME, then remove that event,
             // and do not add the disappear event either.
             if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) {
@@ -4894,6 +4909,7 @@
      * @param bottomBarHeight the height of the bar on the bottom
      */
     public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
+        SceneContainerFlag.assertInLegacyMode();
         mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
         mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height);
         mStateAnimator.setHeadsUpAppearHeightBottom(height);
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 391bc43..06222fd 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
@@ -74,7 +74,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalismPrototype.V2.isEnabled
+        get() = NotificationMinimalismPrototype.isEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -405,16 +405,8 @@
 
     fun updateResources() {
         maxKeyguardNotifications =
-            infiniteIfNegative(
-                if (NotificationMinimalismPrototype.V1.isEnabled) {
-                    NotificationMinimalismPrototype.V1.maxNotifs
-                } else {
-                    resources.getInteger(R.integer.keyguard_max_notification_count)
-                }
-            )
-        maxNotificationsExcludesMedia =
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
+            infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index 97ec391..383d8b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -32,8 +32,6 @@
 class ScrollViewFields {
     /** Used to produce the clipping path */
     var scrimClippingShape: ShadeScrimShape? = null
-    /** Y coordinate in view pixels of the top of the HUN */
-    var headsUpTop: Float = 0f
     /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
     var isScrolledToTop: Boolean = true
 
@@ -74,7 +72,6 @@
     fun dump(pw: IndentingPrintWriter) {
         pw.printSection("StackViewStates") {
             pw.println("scrimClippingShape", scrimClippingShape)
-            pw.println("headsUpTop", headsUpTop)
             pw.println("isScrolledToTop", isScrolledToTop)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 4282fa2..b801e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -79,9 +79,7 @@
     private int mHeadsUpAppearHeightBottom;
     private int mHeadsUpCyclingPadding;
 
-    public StackScrollAlgorithm(
-            Context context,
-            ViewGroup hostView) {
+    public StackScrollAlgorithm(Context context, ViewGroup hostView) {
         mHostView = hostView;
         initView(context);
     }
@@ -865,7 +863,10 @@
 
         // Move the tracked heads up into position during the appear animation, by interpolating
         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
-        float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin();
+        float headsUpTranslation =
+                SceneContainerFlag.isEnabled()
+                        ? ambientState.getHeadsUpTop()
+                        : mHeadsUpInset - ambientState.getStackTopMargin();
         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
         if (trackedHeadsUpRow != null) {
             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
@@ -894,21 +895,44 @@
             boolean isTopEntry = topHeadsUpEntry == row;
             float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
             if (mIsExpanded) {
-                if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
-                        childState.headsUpIsVisible, row.showingPulsing(),
-                        ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
-                    // Ensure that the heads up is always visible even when scrolled off.
-                    // NSSL y starts at top of screen in non-split-shade, but below the qs offset
-                    // in split shade, so we only need to inset by the scrim padding in split shade.
-                    // TODO(b/332574413) get the clamp inset from HeadsUpNotificationPlaceholder
-                    final float clampInset = ambientState.getUseSplitShade()
-                            ? mNotificationScrimPadding : mQuickQsOffsetHeight;
-                    clampHunToTop(clampInset, ambientState.getStackTranslation(),
-                            row.getCollapsedHeight(), childState);
-                    if (isTopEntry && row.isAboveShelf()) {
-                        // the first hun can't get off screen.
-                        clampHunToMaxTranslation(ambientState, row, childState);
-                        childState.hidden = false;
+                if (SceneContainerFlag.isEnabled()) {
+                    if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+                            childState.headsUpIsVisible, row.showingPulsing(),
+                            ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
+                        clampHunToTop(
+                                /* headsUpTop = */ headsUpTranslation,
+                                /* collapsedHeight = */ row.getCollapsedHeight(),
+                                /* viewState = */ childState
+                        );
+                        if (isTopEntry && row.isAboveShelf()) {
+                            clampHunToMaxTranslation(
+                                    /* headsUpTop =  */ headsUpTranslation,
+                                    /* headsUpBottom =  */ ambientState.getHeadsUpBottom(),
+                                    /* viewState = */ childState
+                            );
+                            updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop());
+                            childState.hidden = false;
+                        }
+                    }
+                } else {
+                    if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+                            childState.headsUpIsVisible, row.showingPulsing(),
+                            ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
+                        // Ensure that the heads up is always visible even when scrolled off.
+                        // NSSL y starts at top of screen in non-split-shade, but below the qs
+                        // offset
+                        // in split shade, so we only need to inset by the scrim padding in split
+                        // shade.
+                        final float clampInset = ambientState.getUseSplitShade()
+                                ? mNotificationScrimPadding : mQuickQsOffsetHeight;
+                        clampHunToTop(clampInset, ambientState.getStackTranslation(),
+                                row.getCollapsedHeight(), childState);
+                        if (isTopEntry && row.isAboveShelf()) {
+                            // the first hun can't get off screen.
+                            clampHunToMaxTranslation(ambientState, row, childState);
+                            updateCornerRoundnessForPinnedHun(row, ambientState.getStackY());
+                            childState.hidden = false;
+                        }
                     }
                 }
             }
@@ -1005,9 +1029,13 @@
     @VisibleForTesting
     void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight,
             ExpandableViewState viewState) {
+        SceneContainerFlag.assertInLegacyMode();
+        clampHunToTop(clampInset + stackTranslation, collapsedHeight, viewState);
+    }
 
-        final float newTranslation = Math.max(clampInset + stackTranslation,
-                viewState.getYTranslation());
+    @VisibleForTesting
+    void clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState) {
+        final float newTranslation = Math.max(headsUpTop, viewState.getYTranslation());
 
         // Transition from collapsed pinned state to fully expanded state
         // when the pinned HUN approaches its actual location (when scrolling back to top).
@@ -1020,9 +1048,12 @@
     // while the rest of notifications are scrolled offscreen.
     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
             ExpandableViewState childState) {
+        SceneContainerFlag.assertInLegacyMode();
         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
-        final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
-                + ambientState.getStackTranslation();
+        final float maxShelfPosition =
+                ambientState.getInnerHeight()
+                        + ambientState.getTopPadding()
+                        + ambientState.getStackTranslation();
         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
 
         final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
@@ -1030,13 +1061,20 @@
         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
                 - newTranslation);
         childState.setYTranslation(newTranslation);
+    }
 
+    private void clampHunToMaxTranslation(float headsUpTop, float headsUpBottom,
+            ExpandableViewState viewState) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        final float maxHeight = headsUpTop - headsUpBottom;
+        viewState.setYTranslation(Math.min(headsUpTop, viewState.getYTranslation()));
+        viewState.height = (int) Math.min(maxHeight, viewState.height);
+    }
+
+    private void updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop) {
         // Animate pinned HUN bottom corners to and from original roundness.
         final float originalCornerRadius =
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
-        final float stackTop = SceneContainerFlag.isEnabled()
-                ? ambientState.getStackTop()
-                : ambientState.getStackY();
         final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                 stackTop, getMaxAllowedChildHeight(row), originalCornerRadius);
         row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 762c507..6226fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -59,6 +59,9 @@
     /** set the y position in px of the top of the HUN in this view's coordinates */
     fun setHeadsUpTop(headsUpTop: Float)
 
+    /** set the bottom-most y position in px, where we can draw HUNs in this view's coordinates */
+    fun setHeadsUpBottom(headsUpBottom: Float)
+
     /** set whether the view has been scrolled all the way to the top */
     fun setScrolledToTop(scrolledToTop: Boolean)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 97266c5..86c7c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
@@ -36,10 +37,16 @@
 constructor(
     private val statusBarStateController: SysuiStatusBarStateController,
     @Main private val mainExecutor: DelayableExecutor,
+    activityStarterInternal: Lazy<ActivityStarterInternalImpl>,
     legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl>
 ) : ActivityStarter {
 
-    private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get()
+    private val activityStarterInternal: ActivityStarterInternal =
+        if (SceneContainerFlag.isEnabled) {
+            activityStarterInternal.get()
+        } else {
+            legacyActivityStarter.get()
+        }
 
     override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) {
         activityStarterInternal.startPendingIntentDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index ae98e1d..107bf1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -16,23 +16,93 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.ActivityTaskManager
 import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Bundle
+import android.os.RemoteException
 import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.RemoteAnimationAdapter
 import android.view.View
+import android.view.WindowManager
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.Flags
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Lazy
+import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
- * Encapsulates the activity logic for activity starter when flexiglass is enabled.
+ * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled.
  *
  * TODO: b/308819693
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
-class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal {
+class ActivityStarterInternalImpl
+@Inject
+constructor(
+    private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
+    private val context: Context,
+    @Main private val resources: Resources,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val activityTransitionAnimator: ActivityTransitionAnimator,
+    @DisplayId private val displayId: Int,
+    private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+    private val activityIntentHelper: ActivityIntentHelper,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val assistManagerLazy: Lazy<AssistManager>,
+    @Main private val mainExecutor: DelayableExecutor,
+    private val shadeControllerLazy: Lazy<ShadeController>,
+    private val communalSceneInteractor: CommunalSceneInteractor,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
+    private val shadeAnimationInteractor: ShadeAnimationInteractor,
+    private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
+    private val commandQueue: CommandQueue,
+    private val lockScreenUserManager: NotificationLockscreenUserManager,
+) : ActivityStarterInternal {
+    private val centralSurfaces: CentralSurfaces?
+        get() = centralSurfacesOptLazy.get().getOrNull()
+
     override fun startPendingIntentDismissingKeyguard(
         intent: PendingIntent,
         dismissShade: Boolean,
@@ -45,7 +115,119 @@
         extraOptions: Bundle?,
         customMessage: String?,
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val animationController =
+            if (associatedView is ExpandableNotificationRow) {
+                centralSurfaces?.getAnimatorControllerFromNotification(associatedView)
+            } else animationController
+
+        val willLaunchResolverActivity =
+            intent.isActivity &&
+                activityIntentHelper.wouldPendingLaunchResolverActivity(
+                    intent,
+                    lockScreenUserManager.currentUserId,
+                )
+
+        val actuallyShowOverLockscreen =
+            showOverLockscreen &&
+                intent.isActivity &&
+                (skipLockscreenChecks ||
+                    activityIntentHelper.wouldPendingShowOverLockscreen(
+                        intent,
+                        lockScreenUserManager.currentUserId
+                    ))
+
+        val animate =
+            !willLaunchResolverActivity &&
+                animationController != null &&
+                shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen)
+
+        // We wrap animationCallback with a StatusBarLaunchAnimatorController so
+        // that the shade is collapsed after the animation (or when it is cancelled,
+        // aborted, etc).
+        val statusBarController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = intent.isActivity,
+            )
+        val controller =
+            if (actuallyShowOverLockscreen) {
+                wrapAnimationControllerForLockscreen(dismissShade, statusBarController)
+            } else {
+                statusBarController
+            }
+
+        // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
+        // run the animation on the keyguard). The animation will take care of (instantly)
+        // collapsing the shade and hiding the keyguard once it is done.
+        val collapse = dismissShade && !animate
+        val runnable = Runnable {
+            try {
+                activityTransitionAnimator.startPendingIntentWithAnimation(
+                    controller,
+                    animate,
+                    intent.creatorPackage,
+                    actuallyShowOverLockscreen,
+                    object : ActivityTransitionAnimator.PendingIntentStarter {
+                        override fun startPendingIntent(
+                            animationAdapter: RemoteAnimationAdapter?
+                        ): Int {
+                            val options =
+                                ActivityOptions(
+                                    CentralSurfaces.getActivityOptions(displayId, animationAdapter)
+                                        .apply { extraOptions?.let { putAll(it) } }
+                                )
+                            // TODO b/221255671: restrict this to only be set for notifications
+                            options.isEligibleForLegacyPermissionPrompt = true
+                            options.setPendingIntentBackgroundActivityStartMode(
+                                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                            )
+                            return intent.sendAndReturnResult(
+                                context,
+                                0,
+                                fillInIntent,
+                                null,
+                                null,
+                                null,
+                                options.toBundle()
+                            )
+                        }
+                    },
+                )
+            } catch (e: PendingIntent.CanceledException) {
+                // the stack trace isn't very helpful here.
+                // Just log the exception message.
+                Log.w(TAG, "Sending intent failed: $e")
+                if (!collapse) {
+                    // executeRunnableDismissingKeyguard did not collapse for us already.
+                    shadeControllerLazy.get().collapseOnMainThread()
+                }
+                // TODO: Dismiss Keyguard.
+            }
+            if (intent.isActivity) {
+                assistManagerLazy.get().hideAssist()
+                // This activity could have started while the device is dreaming, in which case
+                // the dream would occlude the activity. In order to show the newly started
+                // activity, we wake from the dream.
+                centralSurfaces?.awakenDreams()
+            }
+            intentSentUiThreadCallback?.let { mainExecutor.execute(it) }
+        }
+
+        if (!actuallyShowOverLockscreen) {
+            mainExecutor.execute {
+                executeRunnableDismissingKeyguard(
+                    runnable = runnable,
+                    afterKeyguardGone = willLaunchResolverActivity,
+                    dismissShade = collapse,
+                    willAnimateOnKeyguard = animate,
+                    customMessage = customMessage,
+                )
+            }
+        } else {
+            mainExecutor.execute(runnable)
+        }
     }
 
     override fun startActivityDismissingKeyguard(
@@ -59,7 +241,116 @@
         disallowEnterPictureInPictureWhileLaunching: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent)
+
+        if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return
+
+        val willLaunchResolverActivity: Boolean =
+            activityIntentHelper.wouldLaunchResolverActivity(
+                intent,
+                selectedUserInteractor.getSelectedUserId(),
+            )
+
+        val animate =
+            animationController != null &&
+                !willLaunchResolverActivity &&
+                shouldAnimateLaunch(isActivityIntent = true)
+        val animController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = true,
+            )
+
+        // If we animate, we will dismiss the shade only once the animation is done. This is
+        // taken care of by the StatusBarLaunchAnimationController.
+        val dismissShadeDirectly = dismissShade && animController == null
+
+        val runnable = Runnable {
+            assistManagerLazy.get().hideAssist()
+            intent.flags =
+                if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) {
+                    Intent.FLAG_ACTIVITY_NEW_TASK
+                } else {
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+                }
+            intent.addFlags(flags)
+            val result = intArrayOf(ActivityManager.START_CANCELED)
+            activityTransitionAnimator.startIntentWithAnimation(
+                animController,
+                animate,
+                intent.getPackage()
+            ) { adapter: RemoteAnimationAdapter? ->
+                val options =
+                    ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter))
+
+                // We know that the intent of the caller is to dismiss the keyguard and
+                // this runnable is called right after the keyguard is solved, so we tell
+                // WM that we should dismiss it to avoid flickers when opening an activity
+                // that can also be shown over the keyguard.
+                options.setDismissKeyguardIfInsecure()
+                options.setDisallowEnterPictureInPictureWhileLaunching(
+                    disallowEnterPictureInPictureWhileLaunching
+                )
+                if (CameraIntents.isInsecureCameraIntent(intent)) {
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the cross fade animation if an orientation change
+                    // happens to occur during the launch.
+                    options.rotationAnimationHint =
+                        WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+                }
+                if (Settings.Panel.ACTION_VOLUME == intent.action) {
+                    // Settings Panel is implemented as activity(not a dialog), so
+                    // underlying app is paused and may enter picture-in-picture mode
+                    // as a result.
+                    // So we need to disable picture-in-picture mode here
+                    // if it is volume panel.
+                    options.setDisallowEnterPictureInPictureWhileLaunching(true)
+                }
+                try {
+                    result[0] =
+                        ActivityTaskManager.getService()
+                            .startActivityAsUser(
+                                null,
+                                context.basePackageName,
+                                context.attributionTag,
+                                intent,
+                                intent.resolveTypeIfNeeded(context.contentResolver),
+                                null,
+                                null,
+                                0,
+                                Intent.FLAG_ACTIVITY_NEW_TASK,
+                                null,
+                                options.toBundle(),
+                                userHandle.identifier,
+                            )
+                } catch (e: RemoteException) {
+                    Log.w(TAG, "Unable to start activity", e)
+                }
+                result[0]
+            }
+            callback?.onActivityStarted(result[0])
+        }
+        val cancelRunnable = Runnable {
+            callback?.onActivityStarted(ActivityManager.START_CANCELED)
+        }
+        // Do not deferKeyguard when occluded because, when keyguard is occluded,
+        // we do not launch the activity until keyguard is done.
+        executeRunnableDismissingKeyguard(
+            runnable,
+            cancelRunnable,
+            dismissShadeDirectly,
+            willLaunchResolverActivity,
+            deferred = !isKeyguardOccluded(),
+            animate,
+            customMessage,
+        )
     }
 
     override fun startActivity(
@@ -69,7 +360,64 @@
         showOverLockscreenWhenLocked: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle = userHandle ?: getActivityUserHandle(intent)
+        // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't
+        // want to show the activity above it.
+        if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) {
+            startActivityDismissingKeyguard(
+                intent = intent,
+                onlyProvisioned = false,
+                dismissShade = dismissShade,
+                disallowEnterPictureInPictureWhileLaunching = false,
+                callback = null,
+                flags = 0,
+                animationController = animationController,
+                userHandle = userHandle,
+            )
+            return
+        }
+
+        val animate =
+            animationController != null &&
+                shouldAnimateLaunch(
+                    isActivityIntent = true,
+                    showOverLockscreen = showOverLockscreenWhenLocked
+                )
+
+        var controller: ActivityTransitionAnimator.Controller? = null
+        if (animate) {
+            // Wrap the animation controller to dismiss the shade and set
+            // mIsLaunchingActivityOverLockscreen during the animation.
+            val delegate =
+                wrapAnimationControllerForShadeOrStatusBar(
+                    animationController = animationController,
+                    dismissShade = dismissShade,
+                    isLaunchForActivity = true,
+                )
+            controller = wrapAnimationControllerForLockscreen(dismissShade, delegate)
+        } else if (dismissShade) {
+            // The animation will take care of dismissing the shade at the end of the animation.
+            // If we don't animate, collapse it directly.
+            shadeControllerLazy.get().cancelExpansionAndCollapseShade()
+        }
+
+        // We should exit the dream to prevent the activity from starting below the
+        // dream.
+        if (keyguardInteractor.isDreaming.value) {
+            centralSurfaces?.awakenDreams()
+        }
+
+        activityTransitionAnimator.startIntentWithAnimation(
+            controller,
+            animate,
+            intent.getPackage(),
+            showOverLockscreenWhenLocked
+        ) { adapter: RemoteAnimationAdapter? ->
+            TaskStackBuilder.create(context)
+                .addNextIntent(intent)
+                .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle)
+        }
     }
 
     override fun dismissKeyguardThenExecute(
@@ -78,7 +426,23 @@
         afterKeyguardGone: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone")
+
+        // TODO b/308819693: startWakeAndUnlock animation when pulsing
+
+        if (isKeyguardShowing()) {
+            statusBarKeyguardViewManagerLazy
+                .get()
+                .dismissWithAction(action, cancel, afterKeyguardGone, customMessage)
+        } else {
+            // If the keyguard isn't showing but the device is dreaming, we should exit the
+            // dream.
+            if (keyguardInteractor.isDreaming.value) {
+                centralSurfaces?.awakenDreams()
+            }
+            action.onDismiss()
+        }
     }
 
     override fun executeRunnableDismissingKeyguard(
@@ -90,10 +454,195 @@
         willAnimateOnKeyguard: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val onDismissAction: ActivityStarter.OnDismissAction =
+            object : ActivityStarter.OnDismissAction {
+                override fun onDismiss(): Boolean {
+                    if (runnable != null) {
+                        if (isKeyguardOccluded()) {
+                            statusBarKeyguardViewManagerLazy
+                                .get()
+                                .addAfterKeyguardGoneRunnable(runnable)
+                        } else {
+                            mainExecutor.execute(runnable)
+                        }
+                    }
+                    if (dismissShade) {
+                        shadeControllerLazy.get().collapseShadeForActivityStart()
+                    }
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard()
+                    }
+                    return deferred
+                }
+
+                override fun willRunAnimationOnKeyguard(): Boolean {
+                    if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) {
+                        // Override to false when launching activity over the hub that requires auth
+                        return false
+                    }
+                    return willAnimateOnKeyguard
+                }
+            }
+        dismissKeyguardThenExecute(
+            onDismissAction,
+            cancelAction,
+            afterKeyguardGone,
+            customMessage,
+        )
     }
 
     override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean {
-        TODO("Not yet implemented b/308819693")
+        return shouldAnimateLaunch(isActivityIntent, false)
+    }
+
+    /**
+     * Whether we should animate an activity launch.
+     *
+     * Note: This method must be called *before* dismissing the keyguard.
+     */
+    private fun shouldAnimateLaunch(
+        isActivityIntent: Boolean,
+        showOverLockscreen: Boolean,
+    ): Boolean {
+        // TODO(b/294418322): always support launch animations when occluded.
+        val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation()
+        if (isKeyguardOccluded() && !ignoreOcclusion) {
+            return false
+        }
+
+        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+        // (without unlocking it).
+        if (showOverLockscreen || !isKeyguardShowing()) {
+            return true
+        }
+
+        // We don't animate non-activity launches as they can break the animation.
+        // TODO(b/184121838): Support non activity launches on the lockscreen.
+        return isActivityIntent
+    }
+
+    /** Retrieves the current user handle to start the Activity. */
+    private fun getActivityUserHandle(intent: Intent): UserHandle {
+        val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages)
+        for (pkg in packages) {
+            val componentName = intent.component ?: break
+            if (pkg == componentName.packageName) {
+                return UserHandle(UserHandle.myUserId())
+            }
+        }
+        return UserHandle(selectedUserInteractor.getSelectedUserId())
+    }
+
+    private fun isKeyguardShowing(): Boolean {
+        return !deviceEntryInteractor.isDeviceEntered.value
+    }
+
+    private fun isKeyguardOccluded(): Boolean {
+        return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED
+    }
+
+    /**
+     * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that:
+     * - if it launches in the notification shade window and `dismissShade` is true, then the shade
+     *   will be instantly dismissed at the end of the animation.
+     * - if it launches in status bar window, it will make the status bar window match the device
+     *   size during the animation (that way, the animation won't be clipped by the status bar
+     *   size).
+     *
+     * @param animationController the controller that is wrapped and will drive the main animation.
+     * @param dismissShade whether the notification shade will be dismissed at the end of the
+     *   animation. This is ignored if `animationController` is not animating in the shade window.
+     * @param isLaunchForActivity whether the launch is for an activity.
+     */
+    private fun wrapAnimationControllerForShadeOrStatusBar(
+        animationController: ActivityTransitionAnimator.Controller?,
+        dismissShade: Boolean,
+        isLaunchForActivity: Boolean,
+    ): ActivityTransitionAnimator.Controller? {
+        if (animationController == null) {
+            return null
+        }
+        val rootView = animationController.transitionContainer.rootView
+        val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> =
+            statusBarWindowController.wrapAnimationControllerIfInStatusBar(
+                rootView,
+                animationController
+            )
+        if (controllerFromStatusBar.isPresent) {
+            return controllerFromStatusBar.get()
+        }
+
+        centralSurfaces?.let {
+            // If the view is not in the status bar, then we are animating a view in the shade.
+            // We have to make sure that we collapse it when the animation ends or is cancelled.
+            if (dismissShade) {
+                return StatusBarTransitionAnimatorController(
+                    animationController,
+                    shadeAnimationInteractor,
+                    shadeControllerLazy.get(),
+                    notifShadeWindowControllerLazy.get(),
+                    commandQueue,
+                    displayId,
+                    isLaunchForActivity
+                )
+            }
+        }
+
+        return animationController
+    }
+
+    /**
+     * Wraps an animation controller so that if an activity would be launched on top of the
+     * lockscreen, the correct flags are set for it to be occluded.
+     */
+    private fun wrapAnimationControllerForLockscreen(
+        dismissShade: Boolean,
+        animationController: ActivityTransitionAnimator.Controller?
+    ): ActivityTransitionAnimator.Controller? {
+        return animationController?.let {
+            object : DelegateTransitionAnimatorController(it) {
+                override fun onIntentStarted(willAnimate: Boolean) {
+                    delegate.onIntentStarted(willAnimate)
+                    if (willAnimate) {
+                        centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade)
+                    }
+                }
+
+                override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+                    super.onTransitionAnimationStart(isExpandingFullyAbove)
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.snapToScene(
+                            CommunalScenes.Blank,
+                            ActivityTransitionAnimator.TIMINGS.totalDuration
+                        )
+                    }
+                }
+
+                override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the post collapse runnables)
+                    // later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+                }
+
+                override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the // post collapse
+                    // runnables) later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "ActivityStarterInternalImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index a2e44df..789a6f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -39,11 +39,13 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener;
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.AnimationStateHandler;
@@ -86,7 +88,7 @@
     private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>();
     private final VisualStabilityProvider mVisualStabilityProvider;
 
-    private final AvalancheController mAvalancheController;
+    private AvalancheController mAvalancheController;
 
     // TODO(b/328393698) move the topHeadsUpRow logic to an interactor
     private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow =
@@ -173,6 +175,12 @@
         });
         javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
                     this::onShadeOrQsExpanded);
+        if (NotificationThrottleHun.isEnabled()) {
+            mVisualStabilityProvider.addPersistentReorderingBannedListener(
+                    mOnReorderingBannedListener);
+            mVisualStabilityProvider.addPersistentReorderingAllowedListener(
+                    mOnReorderingAllowedListener);
+        }
     }
 
     public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -379,6 +387,9 @@
 
     private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> {
         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false);
+        if (NotificationThrottleHun.isEnabled()) {
+            mAvalancheController.setEnableAtRuntime(true);
+        }
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
@@ -389,6 +400,22 @@
         mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true);
     };
 
+    private final OnReorderingBannedListener mOnReorderingBannedListener = () -> {
+        if (mAvalancheController != null) {
+            // In open shade the first HUN is pinned, and visual stability logic prevents us from
+            // unpinning this first HUN as long as the shade remains open. AvalancheController only
+            // shows the next HUN when the currently showing HUN is unpinned, so we must disable
+            // throttling here so that the incoming HUN stream is not forever paused. This is reset
+            // when reorder becomes allowed.
+            mAvalancheController.setEnableAtRuntime(false);
+
+            // Note that we cannot do the above when
+            // 1) The remove runnable runs because its delay means it may not run before shade close
+            // 2) Reordering is allowed again (when shade closes) because the HUN appear animation
+            // will have started by then
+        }
+    };
+
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  HeadsUpManager utility (protected) methods overrides:
 
@@ -561,18 +588,36 @@
         }
 
         @Override
-        protected Runnable createRemoveRunnable(NotificationEntry entry) {
-            return  () -> {
+        protected void setEntry(@androidx.annotation.NonNull NotificationEntry entry,
+                @androidx.annotation.Nullable Runnable removeRunnable) {
+            super.setEntry(entry, removeRunnable);
+
+            if (NotificationThrottleHun.isEnabled()) {
                 if (!mVisualStabilityProvider.isReorderingAllowed()
                         // We don't want to allow reordering while pulsing, but headsup need to
                         // time out anyway
                         && !entry.showingPulsing()) {
                     mEntriesToRemoveWhenReorderingAllowed.add(entry);
+                    entry.setSeenInShade(true);
+                }
+            }
+        }
+
+        @Override
+        protected Runnable createRemoveRunnable(NotificationEntry entry) {
+            return () -> {
+                if (!NotificationThrottleHun.isEnabled()
+                        && !mVisualStabilityProvider.isReorderingAllowed()
+                        // We don't want to allow reordering while pulsing, but headsup need to
+                        // time out anyway
+                        && !entry.showingPulsing()) {
+                    mEntriesToRemoveWhenReorderingAllowed.add(entry);
                     mVisualStabilityProvider.addTemporaryReorderingAllowedListener(
                             mOnReorderingAllowedListener);
                 } else if (mTrackingHeadsUp) {
                     mEntriesToRemoveAfterExpand.add(entry);
-                } else {
+                } else if (mVisualStabilityProvider.isReorderingAllowed()
+                        || entry.showingPulsing()) {
                     removeEntry(entry.getKey(), "createRemoveRunnable");
                 }
             };
@@ -585,8 +630,10 @@
             if (mEntriesToRemoveAfterExpand.contains(mEntry)) {
                 mEntriesToRemoveAfterExpand.remove(mEntry);
             }
-            if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
-                mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
+            if (!NotificationThrottleHun.isEnabled()) {
+                if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) {
+                    mEntriesToRemoveWhenReorderingAllowed.remove(mEntry);
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index e69a78f..1a47081 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -40,7 +40,6 @@
 import com.android.systemui.camera.CameraIntents
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
-import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.DisplayId
 import com.android.systemui.dagger.qualifiers.Main
@@ -208,10 +207,16 @@
         val cancelRunnable = Runnable {
             callback?.onActivityStarted(ActivityManager.START_CANCELED)
         }
-        // Do not deferKeyguard when occluded because, when keyguard is occluded,
-        // we do not launch the activity until keyguard is done.
+        // Do not deferKeyguard when occluded because, when keyguard is occluded, we do not launch
+        // the activity until keyguard is done. The only exception is when we're on the Hub and want
+        // to dismiss the shade immediately, which means that another animation will take care of
+        // the transition.
         val occluded = (keyguardStateController.isShowing && keyguardStateController.isOccluded)
-        val deferred = !occluded
+        val dismissOnCommunal =
+            communalSettingsInteractor.isCommunalFlagEnabled() &&
+                communalSceneInteractor.isCommunalVisible.value &&
+                dismissShadeDirectly
+        val deferred = !occluded || dismissOnCommunal
         executeRunnableDismissingKeyguard(
             runnable,
             cancelRunnable,
@@ -463,10 +468,18 @@
             object : ActivityStarter.OnDismissAction {
                 override fun onDismiss(): Boolean {
                     if (runnable != null) {
+                        // We don't wait for Keyguard to be gone if we're dismissing the shade
+                        // immediately and we're on the Communal Hub. This is to make sure that the
+                        // Hub -> Edit Mode transition is seamless.
+                        val dismissOnCommunal =
+                            communalSettingsInteractor.isCommunalFlagEnabled() &&
+                                communalSceneInteractor.isCommunalVisible.value &&
+                                dismissShade
                         if (
                             keyguardStateController.isShowing &&
                                 keyguardStateController.isOccluded &&
-                                !isCommunalWidgetLaunch()
+                                !isCommunalWidgetLaunch() &&
+                                !dismissOnCommunal
                         ) {
                             statusBarKeyguardViewManagerLazy
                                 .get()
@@ -562,12 +575,6 @@
 
                 override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
                     super.onTransitionAnimationStart(isExpandingFullyAbove)
-                    if (communalSettingsInteractor.isCommunalFlagEnabled()) {
-                        communalSceneInteractor.snapToScene(
-                            CommunalScenes.Blank,
-                            ActivityTransitionAnimator.TIMINGS.totalDuration
-                        )
-                    }
                     // Double check that the keyguard is still showing and not going
                     // away, but if so set the keyguard occluded. Typically, WM will let
                     // KeyguardViewMediator know directly, but we're overriding that to
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 aced0be..0320a7a 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
@@ -528,9 +528,10 @@
                 }
 
                 @Override
-                public void onOngoingActivityStatusChanged(boolean hasOngoingActivity) {
+                public void onOngoingActivityStatusChanged(
+                        boolean hasOngoingActivity, boolean shouldAnimate) {
                     mHasOngoingActivity = hasOngoingActivity;
-                    updateStatusBarVisibilities(/* animate= */ true);
+                    updateStatusBarVisibilities(shouldAnimate);
                 }
 
                 @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index ae1898b..16bd7f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -110,7 +110,7 @@
                                     chipView.setOnClickListener(chipModel.onClickListener)
 
                                     // Accessibility
-                                    setChipAccessibility(chipModel, chipView)
+                                    setChipAccessibility(chipModel, chipView, chipBackgroundView)
 
                                     // Colors
                                     val textColor = chipModel.colors.text(chipContext)
@@ -122,7 +122,8 @@
 
                                     // Notify listeners
                                     listener.onOngoingActivityStatusChanged(
-                                        hasOngoingActivity = true
+                                        hasOngoingActivity = true,
+                                        shouldAnimate = true,
                                     )
                                 }
                                 is OngoingActivityChipModel.Hidden -> {
@@ -130,7 +131,8 @@
                                     // b/192243808 and [Chronometer.start].
                                     chipTimeView.stop()
                                     listener.onOngoingActivityStatusChanged(
-                                        hasOngoingActivity = false
+                                        hasOngoingActivity = false,
+                                        shouldAnimate = chipModel.shouldAnimate,
                                     )
                                 }
                             }
@@ -211,7 +213,11 @@
         this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
     }
 
-    private fun setChipAccessibility(chipModel: OngoingActivityChipModel.Shown, chipView: View) {
+    private fun setChipAccessibility(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipView: View,
+        chipBackgroundView: View,
+    ) {
         when (chipModel) {
             is OngoingActivityChipModel.Shown.Countdown -> {
                 // Set as assertive so talkback will announce the countdown
@@ -222,6 +228,16 @@
                 chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE
             }
         }
+        // Clickable chips need to be a minimum size for accessibility purposes, but let
+        // non-clickable chips be smaller.
+        if (chipModel.onClickListener != null) {
+            chipBackgroundView.minimumWidth =
+                chipBackgroundView.context.resources.getDimensionPixelSize(
+                    R.dimen.min_clickable_item_size
+                )
+        } else {
+            chipBackgroundView.minimumWidth = 0
+        }
     }
 
     private fun animateLightsOutView(view: View, visible: Boolean) {
@@ -266,8 +282,13 @@
     /** Called when a transition from lockscreen to dream has started. */
     fun onTransitionFromLockscreenToDreamStarted()
 
-    /** Called when the status of the ongoing activity chip (active or not active) has changed. */
-    fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean)
+    /**
+     * Called when the status of the ongoing activity chip (active or not active) has changed.
+     *
+     * @param shouldAnimate true if the chip should animate in/out, and false if the chip should
+     *   immediately appear/disappear.
+     */
+    fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean, shouldAnimate: Boolean)
 
     /**
      * Called when the scene state has changed such that the home status bar is newly allowed or no
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 40799583..a88c6d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -37,13 +37,27 @@
 @SysUISingleton
 class AvalancheController
 @Inject
-constructor(dumpManager: DumpManager,
-            private val uiEventLogger: UiEventLogger,
-            @Background private val bgHandler: Handler
+constructor(
+    dumpManager: DumpManager,
+    private val uiEventLogger: UiEventLogger,
+    @Background private val bgHandler: Handler
 ) : Dumpable {
 
     private val tag = "AvalancheController"
     private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
+    var enableAtRuntime = true
+        set(value) {
+            if (!value) {
+                // Waiting HUNs in AvalancheController are shown in the HUN section in open shade.
+                // Clear them so we don't show them again when the shade closes and reordering is
+                // allowed again.
+                logDroppedHunsInBackground(getWaitingKeys().size)
+                clearNext()
+            }
+            if (field != value) {
+                field = value
+            }
+        }
 
     // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD
     @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null
@@ -90,13 +104,17 @@
         return getKey(headsUpEntryShowing)
     }
 
+    fun isEnabled(): Boolean {
+        return NotificationThrottleHun.isEnabled && enableAtRuntime
+    }
+
     /** Run or delay Runnable for given HeadsUpEntry */
     fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) {
         if (runnable == null) {
             log { "Runnable is NULL, stop update." }
             return
         }
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             runnable.run()
             return
         }
@@ -156,7 +174,7 @@
             log { "Runnable is NULL, stop delete." }
             return
         }
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             runnable.run()
             return
         }
@@ -185,7 +203,8 @@
             showNext()
             runnable.run()
         } else {
-            log { "$fn => removing untracked ${getKey(entry)}" }
+            log { "$fn => run runnable for untracked shown ${getKey(entry)}" }
+            runnable.run()
         }
         logState("after $fn")
     }
@@ -197,7 +216,7 @@
      *    BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration.
      */
     fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             // Use default duration, like we did before AvalancheController existed
             return autoDismissMs
         }
@@ -246,7 +265,7 @@
 
     /** Return true if entry is waiting to show. */
     fun isWaiting(key: String): Boolean {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return false
         }
         for (entry in nextMap.keys) {
@@ -259,7 +278,7 @@
 
     /** Return list of keys for huns waiting */
     fun getWaitingKeys(): MutableList<String> {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return mutableListOf()
         }
         val keyList = mutableListOf<String>()
@@ -270,7 +289,7 @@
     }
 
     fun getWaitingEntry(key: String): HeadsUpEntry? {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return null
         }
         for (headsUpEntry in nextMap.keys) {
@@ -282,7 +301,7 @@
     }
 
     fun getWaitingEntryList(): List<HeadsUpEntry> {
-        if (!NotificationThrottleHun.isEnabled) {
+        if (!isEnabled()) {
             return mutableListOf()
         }
         return nextMap.keys.toList()
@@ -340,13 +359,15 @@
         showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList)
     }
 
-    fun logDroppedHunsInBackground(numDropped: Int) {
-        bgHandler.post(Runnable {
-            // Do this in the background to avoid missing frames when closing the shade
-            for (n in 1..numDropped) {
-                uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED)
+    private fun logDroppedHunsInBackground(numDropped: Int) {
+        bgHandler.post(
+            Runnable {
+                // Do this in the background to avoid missing frames when closing the shade
+                for (n in 1..numDropped) {
+                    uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED)
+                }
             }
-        })
+        )
     }
 
     fun clearNext() {
@@ -367,7 +388,8 @@
             "\nPREVIOUS: [$previousHunKey]" +
             "\nNEXT LIST: $nextListStr" +
             "\nNEXT MAP: $nextMapStr" +
-            "\nDROPPED: $dropSetStr"
+            "\nDROPPED: $dropSetStr" +
+            "\nENABLED: $enableAtRuntime"
     }
 
     private fun logState(reason: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 220e729..a0eb989 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -756,7 +756,7 @@
             setEntry(entry, createRemoveRunnable(entry));
         }
 
-        private void setEntry(@NonNull final NotificationEntry entry,
+        protected void setEntry(@NonNull final NotificationEntry entry,
                 @Nullable Runnable removeRunnable) {
             mEntry = entry;
             mRemoveRunnable = removeRunnable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 4838554..07bbca7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -31,6 +31,13 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean>
+
+    /**
+     * Whether this device has been provisioned.
+     *
+     * @see android.provider.Settings.Global.DEVICE_PROVISIONED
+     */
+    fun isDeviceProvisioned(): Boolean
 }
 
 @Module
@@ -48,11 +55,15 @@
         val listener =
             object : DeviceProvisionedController.DeviceProvisionedListener {
                 override fun onDeviceProvisionedChanged() {
-                    trySend(deviceProvisionedController.isDeviceProvisioned)
+                    trySend(isDeviceProvisioned())
                 }
             }
         deviceProvisionedController.addCallback(listener)
-        trySend(deviceProvisionedController.isDeviceProvisioned)
+        trySend(isDeviceProvisioned())
         awaitClose { deviceProvisionedController.removeCallback(listener) }
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return deviceProvisionedController.isDeviceProvisioned
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
index 66ed092..ace4ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -26,7 +26,7 @@
 class DeviceProvisioningInteractor
 @Inject
 constructor(
-    repository: DeviceProvisioningRepository,
+    private val repository: DeviceProvisioningRepository,
 ) {
     /**
      * Whether this device has been provisioned.
@@ -34,4 +34,8 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
+
+    fun isDeviceProvisioned(): Boolean {
+        return repository.isDeviceProvisioned()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index e4d0668..7a521a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -16,8 +16,14 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
+import android.content.Context
 import android.provider.Settings
+import androidx.concurrent.futures.await
 import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.settingslib.notification.modes.ZenIconLoader
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.common.shared.model.Icon
+import java.time.Duration
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -28,7 +34,9 @@
  * An interactor that performs business logic related to the status and configuration of Zen Mode
  * (or Do Not Disturb/DND Mode).
  */
-class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) {
+class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) {
+    private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance()
+
     val isZenModeEnabled: Flow<Boolean> =
         repository.globalZenMode
             .map {
@@ -52,4 +60,18 @@
                 }
             }
             .distinctUntilChanged()
+
+    val modes: Flow<List<ZenMode>> = repository.modes
+
+    suspend fun getModeIcon(mode: ZenMode, context: Context): Icon {
+        return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null)
+    }
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null) {
+        repository.activateMode(zenMode, duration)
+    }
+
+    fun deactivateMode(zenMode: ZenMode) {
+        repository.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
new file mode 100644
index 0000000..2b094d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.ui.dialog
+
+import android.content.Intent
+import android.provider.Settings
+import androidx.compose.material3.Text
+import androidx.compose.ui.res.stringResource
+import com.android.compose.PlatformButton
+import com.android.compose.PlatformOutlinedButton
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.dialog.ui.composable.AlertDialogContent
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.create
+import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+import javax.inject.Inject
+
+class ModesDialogDelegate
+@Inject
+constructor(
+    private val sysuiDialogFactory: SystemUIDialogFactory,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val activityStarter: ActivityStarter,
+    private val viewModel: ModesDialogViewModel,
+) : SystemUIDialog.Delegate {
+    override fun createDialog(): SystemUIDialog {
+        return sysuiDialogFactory.create { dialog ->
+            AlertDialogContent(
+                title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
+                content = { ModeTileGrid(viewModel) },
+                neutralButton = {
+                    PlatformOutlinedButton(
+                        onClick = {
+                            val animationController =
+                                dialogTransitionAnimator.createActivityTransitionController(
+                                    dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)
+                                )
+                            if (animationController == null) {
+                                // The controller will take care of dismissing for us after the
+                                // animation, but let's make sure we dismiss the dialog if we don't
+                                // animate it.
+                                dialog.dismiss()
+                            }
+                            activityStarter.startActivity(
+                                ZEN_MODE_SETTINGS_INTENT,
+                                true /* dismissShade */,
+                                animationController
+                            )
+                        }
+                    ) {
+                        Text(stringResource(R.string.zen_modes_dialog_settings))
+                    }
+                },
+                positiveButton = {
+                    PlatformButton(onClick = { dialog.dismiss() }) {
+                        Text(stringResource(R.string.zen_modes_dialog_done))
+                    }
+                },
+            )
+        }
+    }
+
+    companion object {
+        private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
new file mode 100644
index 0000000..91bfdff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.ui.dialog.composable
+
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel
+
+@Composable
+fun ModeTile(viewModel: ModeTileViewModel) {
+    val tileColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.primary
+        else MaterialTheme.colorScheme.surfaceVariant
+    val contentColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary
+        else MaterialTheme.colorScheme.onSurfaceVariant
+
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        Surface(
+            color = tileColor,
+            shape = RoundedCornerShape(16.dp),
+            modifier =
+                Modifier.combinedClickable(
+                    onClick = viewModel.onClick,
+                    onLongClick = viewModel.onLongClick
+                ),
+        ) {
+            Row(
+                modifier = Modifier.padding(20.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement =
+                    Arrangement.spacedBy(
+                        space = 10.dp,
+                        alignment = Alignment.Start,
+                    ),
+            ) {
+                Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp))
+                Column {
+                    Text(
+                        viewModel.text,
+                        fontWeight = FontWeight.W500,
+                        modifier = Modifier.tileMarquee()
+                    )
+                    Text(
+                        viewModel.subtext,
+                        fontWeight = FontWeight.W400,
+                        modifier = Modifier.tileMarquee()
+                    )
+                }
+            }
+        }
+    }
+}
+
+private fun Modifier.tileMarquee(): Modifier {
+    return this.basicMarquee(
+        iterations = 1,
+        initialDelayMillis = 200,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
new file mode 100644
index 0000000..73d361f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.ui.dialog.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+
+@Composable
+fun ModeTileGrid(viewModel: ModesDialogViewModel) {
+    val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList())
+
+    // TODO(b/346519570): Handle what happens when we have more than a few modes.
+    LazyVerticalGrid(
+        columns = GridCells.Fixed(2),
+        modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        items(
+            tiles.size,
+            key = { index -> tiles[index].id },
+        ) { index ->
+            ModeTile(viewModel = tiles[index])
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
new file mode 100644
index 0000000..5bd26cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes
+ * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the
+ * dialog.
+ */
+data class ModeTileViewModel(
+    val id: String,
+    val icon: Icon,
+    val text: String,
+    val subtext: String,
+    val enabled: Boolean,
+    val contentDescription: String,
+    val onClick: () -> Unit,
+    val onLongClick: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
new file mode 100644
index 0000000..e84c8b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
+
+import android.content.Context
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows
+ * the user to quickly toggle modes.
+ */
+@SysUISingleton
+class ModesDialogViewModel
+@Inject
+constructor(
+    val context: Context,
+    zenModeInteractor: ZenModeInteractor,
+    @Background val bgDispatcher: CoroutineDispatcher,
+) {
+    // Modes that should be displayed in the dialog
+    // TODO(b/346519570): Include modes that have not been set up yet.
+    private val visibleModes: Flow<List<ZenMode>> =
+        zenModeInteractor.modes.map {
+            it.filter { mode ->
+                mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed)
+            }
+        }
+
+    val tiles: Flow<List<ModeTileViewModel>> =
+        visibleModes
+            .map { modesList ->
+                modesList.map { mode ->
+                    ModeTileViewModel(
+                        id = mode.id,
+                        icon = zenModeInteractor.getModeIcon(mode, context),
+                        text = mode.rule.name,
+                        subtext = getTileSubtext(mode),
+                        enabled = mode.isActive,
+                        // TODO(b/346519570): This should be some combination of the above, e.g.
+                        //  "ON: Do Not Disturb, Until Mon 08:09"; see DndTile.
+                        contentDescription = "",
+                        onClick = {
+                            if (mode.isActive) {
+                                zenModeInteractor.deactivateMode(mode)
+                            } else {
+                                // TODO(b/346519570): Handle duration for DND mode.
+                                zenModeInteractor.activateMode(mode)
+                            }
+                        },
+                        onLongClick = {
+                            // TODO(b/346519570): Open settings page for mode.
+                        }
+                    )
+                }
+            }
+            .flowOn(bgDispatcher)
+
+    private fun getTileSubtext(mode: ZenMode): String {
+        // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND
+        val on = context.resources.getString(R.string.zen_mode_on)
+        val off = context.resources.getString(R.string.zen_mode_off)
+        return mode.rule.triggerDescription ?: if (mode.isActive) on else off
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index c45f98e..066bfc5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -18,6 +18,8 @@
 
 import static android.media.AudioManager.RINGER_MODE_NORMAL;
 
+import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
+
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
@@ -59,6 +61,8 @@
 import android.view.accessibility.CaptioningManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Observer;
 
 import com.android.internal.annotations.GuardedBy;
@@ -76,6 +80,8 @@
 import com.android.systemui.util.RingerModeLiveData;
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.concurrency.ThreadFactory;
+import com.android.systemui.util.kotlin.JavaAdapter;
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -102,7 +108,13 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000;
-    private static final int DYNAMIC_STREAM_START_INDEX = 100;
+    // We only need one dynamic stream for broadcast because at most two headsets are allowed
+    // to join local broadcast in current stage.
+    // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio
+    // streams defined in AudioSystem for now and audio team is in the middle of restructure,
+    // no new default stream is preferred.
+    @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99;
+    private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
     private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
             new AudioAttributes.Builder()
                     .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -145,6 +157,8 @@
     private final State mState = new State();
     protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
     private final VibratorHelper mVibrator;
+    private final AudioSharingInteractor mAudioSharingInteractor;
+    private final JavaAdapter mJavaAdapter;
     private final boolean mHasVibrator;
     private boolean mShowA11yStream;
     private boolean mShowVolumeDialog;
@@ -188,7 +202,9 @@
             KeyguardManager keyguardManager,
             ActivityManager activityManager,
             UserTracker userTracker,
-            DumpManager dumpManager
+            DumpManager dumpManager,
+            AudioSharingInteractor audioSharingInteractor,
+            JavaAdapter javaAdapter
     ) {
         mContext = context.getApplicationContext();
         mPackageManager = packageManager;
@@ -200,6 +216,8 @@
         mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
         mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext);
         mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW);
+        mAudioSharingInteractor = audioSharingInteractor;
+        mJavaAdapter = javaAdapter;
         mAudio = audioManager;
         mNoMan = notificationManager;
         mObserver = new SettingObserver(mWorker);
@@ -272,6 +290,12 @@
         } catch (SecurityException e) {
             Log.w(TAG, "No access to media sessions", e);
         }
+        if (volumeDialogAudioSharingFix()) {
+            Slog.d(TAG, "Start collect volume changes in audio sharing");
+            mJavaAdapter.alwaysCollectFlow(
+                    mAudioSharingInteractor.getVolume(),
+                    this::handleAudioSharingStreamVolumeChanges);
+        }
     }
 
     public void setVolumePolicy(VolumePolicy policy) {
@@ -545,7 +569,13 @@
         mState.activeStream = activeStream;
         Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream);
         if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream);
-        final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1;
+        final int s =
+                activeStream
+                                < (volumeDialogAudioSharingFix()
+                                        ? DYNAMIC_STREAM_BROADCAST
+                                        : DYNAMIC_STREAM_REMOTE_START_INDEX)
+                        ? activeStream
+                        : -1;
         if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s);
         mAudio.forceVolumeControlStream(s);
         return true;
@@ -726,7 +756,12 @@
 
     private void onSetStreamVolumeW(int stream, int level) {
         if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level);
-        if (stream >= DYNAMIC_STREAM_START_INDEX) {
+        if (volumeDialogAudioSharingFix() && stream == DYNAMIC_STREAM_BROADCAST) {
+            Slog.d(TAG, "onSetStreamVolumeW set broadcast stream level = " + level);
+            mAudioSharingInteractor.setStreamVolume(level);
+            return;
+        }
+        if (stream >= DYNAMIC_STREAM_REMOTE_START_INDEX) {
             mMediaSessionsCallbacksW.setStreamVolume(stream, level);
             return;
         }
@@ -758,6 +793,40 @@
         DndTile.setVisible(mContext, true);
     }
 
+    void handleAudioSharingStreamVolumeChanges(@Nullable Integer volume) {
+        if (volume == null) {
+            if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) {
+                mState.states.remove(DYNAMIC_STREAM_BROADCAST);
+                Slog.d(TAG, "Remove audio sharing stream");
+                mCallbacks.onStateChanged(mState);
+            }
+        } else {
+            if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) {
+                StreamState ss = mState.states.get(DYNAMIC_STREAM_BROADCAST);
+                if (ss.level != volume) {
+                    ss.level = volume;
+                    Slog.d(TAG, "updateState, audio sharing stream volume = " + volume);
+                    mCallbacks.onStateChanged(mState);
+                }
+            } else {
+                StreamState ss = streamStateW(DYNAMIC_STREAM_BROADCAST);
+                ss.dynamic = true;
+                ss.levelMin = mAudioSharingInteractor.getVolumeMin();
+                ss.levelMax = mAudioSharingInteractor.getVolumeMax();
+                if (ss.level != volume) {
+                    ss.level = volume;
+                }
+                String label = mContext.getString(R.string.audio_sharing_description);
+                if (!Objects.equals(ss.remoteLabel, label)) {
+                    ss.name = -1;
+                    ss.remoteLabel = label;
+                }
+                Slog.d(TAG, "updateState, new audio sharing stream volume = " + volume);
+                mCallbacks.onStateChanged(mState);
+            }
+        }
+    }
+
     private final class VC extends IVolumeController.Stub {
         private final String TAG = VolumeDialogControllerImpl.TAG + ".VC";
 
@@ -1256,7 +1325,7 @@
     protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks {
         private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
 
-        private int mNextStream = DYNAMIC_STREAM_START_INDEX;
+        private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
         private final boolean mVolumeAdjustmentForRemoteGroupSessions;
 
         public MediaSessionsCallbacks(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 6b02e1a..0770d89 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,6 +34,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
+import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix;
 import static com.android.systemui.Flags.hapticVolumeSlider;
 import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
@@ -1678,6 +1679,14 @@
                 return true;
             }
 
+            // Always show the stream for audio sharing if it exists.
+            if (volumeDialogAudioSharingFix()
+                    && row.ss != null
+                    && mContext.getString(R.string.audio_sharing_description)
+                            .equals(row.ss.remoteLabel)) {
+                return true;
+            }
+
             if (row.defaultStream) {
                 return activeRow.stream == STREAM_RING
                         || activeRow.stream == STREAM_ALARM
@@ -1880,10 +1889,25 @@
             if (!ss.dynamic) continue;
             mDynamic.put(stream, true);
             if (findRow(stream) == null) {
-                addRow(stream,
-                        com.android.settingslib.R.drawable.ic_volume_remote,
-                        com.android.settingslib.R.drawable.ic_volume_remote_mute,
-                        true, false, true);
+                if (volumeDialogAudioSharingFix()
+                        && mContext.getString(R.string.audio_sharing_description)
+                                .equals(ss.remoteLabel)) {
+                    addRow(
+                            stream,
+                            R.drawable.ic_volume_media,
+                            R.drawable.ic_volume_media_mute,
+                            true,
+                            false,
+                            true);
+                } else {
+                    addRow(
+                            stream,
+                            com.android.settingslib.R.drawable.ic_volume_remote,
+                            com.android.settingslib.R.drawable.ic_volume_remote_mute,
+                            true,
+                            false,
+                            true);
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt
index 2904092..cf80263 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt
@@ -16,20 +16,15 @@
 
 package com.android.systemui.volume.dagger
 
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
 import com.android.systemui.volume.domain.interactor.AudioSharingInteractorEmptyImpl
+import dagger.Binds
 import dagger.Module
-import dagger.Provides
 
 /** Dagger module for empty audio sharing impl for unnecessary volume overlay */
 @Module
 interface AudioSharingEmptyImplModule {
 
-    companion object {
-        @Provides
-        @SysUISingleton
-        fun provideAudioSharingInteractor(): AudioSharingInteractor =
-            AudioSharingInteractorEmptyImpl()
-    }
+    @Binds
+    fun bindsAudioSharingInteractor(impl: AudioSharingInteractorEmptyImpl): AudioSharingInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
index 4d29788..aba3015 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt
@@ -77,7 +77,7 @@
 }
 
 @SysUISingleton
-class AudioSharingInteractorEmptyImpl : AudioSharingInteractor {
+class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor {
     override val volume: Flow<Int?> = emptyFlow()
     override val volumeMin: Int = EMPTY_VOLUME
     override val volumeMax: Int = EMPTY_VOLUME
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index ec9b5cf..14cd202 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -20,8 +20,9 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -66,6 +67,7 @@
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.onehanded.OneHandedUiEventLogger;
 import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.sysui.ShellInterface;
@@ -249,7 +251,25 @@
                 pip.showPictureInPictureMenu();
             }
         });
+        pip.registerPipTransitionCallback(
+                new PipTransitionController.PipTransitionCallback() {
+                    @Override
+                    public void onPipTransitionStarted(int direction, Rect pipBounds) {
+                        mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, true)
+                                .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+                    }
 
+                    @Override
+                    public void onPipTransitionFinished(int direction) {
+                        mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, false)
+                                .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+                    }
+
+                    @Override
+                    public void onPipTransitionCanceled(int direction) {
+                        // No op.
+                    }
+                }, mSysUiMainExecutor);
         mSysUiState.addCallback(sysUiStateFlag -> {
             mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0;
             pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag);
@@ -263,6 +283,11 @@
             public void onFinishedWakingUp() {
                 splitScreen.onFinishedWakingUp();
             }
+
+            @Override
+            public void onStartedGoingToSleep() {
+                splitScreen.onStartedGoingToSleep();
+            }
         });
         mCommandQueue.addCallback(new CommandQueue.Callbacks() {
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 79933ee..a94ef36 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -16,6 +16,7 @@
 package com.android.keyguard
 
 import android.content.BroadcastReceiver
+import android.platform.test.annotations.DisableFlags
 import android.view.View
 import android.view.ViewTreeObserver
 import android.widget.FrameLayout
@@ -263,9 +264,9 @@
         }
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun keyguardCallback_visibilityChanged_clockDozeCalled() =
         runBlocking(IMMEDIATE) {
-            mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
             val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
             verify(keyguardUpdateMonitor).registerCallback(capture(captor))
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 1d78168..892375d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,6 +29,7 @@
 
 import android.database.ContentObserver;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.provider.Settings;
 import android.view.View;
 
@@ -48,11 +49,10 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchControllerBaseTest {
     @Test
     public void testInit_viewAlreadyAttached() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mController.init();
 
         verifyAttachment(times(1));
@@ -60,8 +60,6 @@
 
     @Test
     public void testInit_viewNotYetAttached() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
 
@@ -78,16 +76,12 @@
 
     @Test
     public void testInitSubControllers() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mController.init();
         verify(mKeyguardSliceViewController).init();
     }
 
     @Test
     public void testInit_viewDetached() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
         mController.init();
@@ -101,8 +95,6 @@
 
     @Test
     public void testPluginPassesStatusBarState() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
 
@@ -116,8 +108,6 @@
 
     @Test
     public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSmartspaceController.isEnabled()).thenReturn(true);
         mController.init();
 
@@ -126,8 +116,6 @@
 
     @Test
     public void onLocaleListChangedRebuildsSmartspaceView() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSmartspaceController.isEnabled()).thenReturn(true);
         mController.init();
 
@@ -138,8 +126,6 @@
 
     @Test
     public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSmartspaceController.isEnabled()).thenReturn(true);
         when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true);
         mController.init();
@@ -153,8 +139,6 @@
 
     @Test
     public void testSmartspaceDisabledShowsKeyguardStatusArea() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
 
@@ -163,8 +147,6 @@
 
     @Test
     public void testRefresh() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mController.refresh();
 
         verify(mSmartspaceController).requestSmartspaceUpdate();
@@ -172,8 +154,6 @@
 
     @Test
     public void testChangeToDoubleLineClockSetsSmallClock() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSecureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1,
                 UserHandle.USER_CURRENT))
                 .thenReturn(0);
@@ -197,15 +177,11 @@
 
     @Test
     public void testGetClock_ForwardsToClock() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         assertEquals(mClockController, mController.getClock());
     }
 
     @Test
     public void testGetLargeClockBottom_returnsExpectedValue() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE);
         when(mLargeClockFrame.getHeight()).thenReturn(100);
         when(mSmallClockFrame.getHeight()).thenReturn(50);
@@ -218,8 +194,6 @@
 
     @Test
     public void testGetSmallLargeClockBottom_returnsExpectedValue() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE);
         when(mLargeClockFrame.getHeight()).thenReturn(100);
         when(mSmallClockFrame.getHeight()).thenReturn(50);
@@ -232,16 +206,12 @@
 
     @Test
     public void testGetClockBottom_nullClock_returnsZero() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mClockEventController.getClock()).thenReturn(null);
         assertEquals(0, mController.getClockBottom(10));
     }
 
     @Test
     public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mSmartspaceController.isWeatherEnabled()).thenReturn(true);
         ArgumentCaptor<ContentObserver> observerCaptor =
                 ArgumentCaptor.forClass(ContentObserver.class);
@@ -260,8 +230,6 @@
 
     @Test
     public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
         when(mSmartspaceController.isEnabled()).thenReturn(true);
@@ -284,15 +252,11 @@
 
     @Test
     public void testGetClock_nullClock_returnsNull() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         when(mClockEventController.getClock()).thenReturn(null);
         assertNull(mController.getClock());
     }
 
     private void verifyAttachment(VerificationMode times) {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
         verify(mClockEventController, times).registerListeners(mView);
@@ -300,8 +264,6 @@
 
     @Test
     public void testSplitShadeEnabledSetToSmartspaceController() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mController.setSplitShadeEnabled(true);
         verify(mSmartspaceController, times(1)).setSplitShadeEnabled(true);
         verify(mSmartspaceController, times(0)).setSplitShadeEnabled(false);
@@ -309,8 +271,6 @@
 
     @Test
     public void testSplitShadeDisabledSetToSmartspaceController() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mController.setSplitShadeEnabled(false);
         verify(mSmartspaceController, times(1)).setSplitShadeEnabled(false);
         verify(mSmartspaceController, times(0)).setSplitShadeEnabled(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 83443be..0bf9d12 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -30,6 +30,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.platform.test.annotations.DisableFlags;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -60,6 +61,7 @@
 // the main thread before acquiring a wake lock. This class is constructed when
 // the keyguard_clock_switch layout is inflated.
 @RunWithLooper(setAsMainLooper = true)
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 public class KeyguardClockSwitchTest extends SysuiTestCase {
     @Mock
     ViewGroup mMockKeyguardSliceView;
@@ -81,8 +83,6 @@
 
     @Before
     public void setUp() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         MockitoAnnotations.initMocks(this);
         when(mMockKeyguardSliceView.getContext()).thenReturn(mContext);
         when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
index b09357f..c51aa04 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java
@@ -151,6 +151,7 @@
 
         if (!SceneContainerFlag.isEnabled()) {
             mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
+            //TODO move this to use @DisableFlags annotation if needed
             mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
index 41a4116..038b81b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java
@@ -216,6 +216,16 @@
     }
 
     @Test
+    public void onRestoreWindowSize_updateSettingsButtonStatusOnRestore() {
+        mMagnification.mWindowMagnifierCallback
+                .onWindowMagnifierBoundsRestored(TEST_DISPLAY, MagnificationSize.SMALL);
+        waitForIdleSync();
+
+        verify(mMagnificationSettingsController)
+                .updateSettingsButtonStatusOnRestore(MagnificationSize.SMALL);
+    }
+
+    @Test
     public void onSetMagnifierSize_delegateToMagnifier() {
         final @MagnificationSize int index = MagnificationSize.SMALL;
         mMagnification.mMagnificationSettingsControllerCallback.onSetMagnifierSize(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
index e272682..e01366a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -67,9 +67,8 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
 import android.testing.TestableLooper;
 import android.testing.TestableResources;
@@ -130,14 +129,12 @@
 @LargeTest
 @TestableLooper.RunWithLooper
 @RunWith(AndroidJUnit4.class)
-@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
+@EnableFlags(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER)
 public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
 
     @Rule
     // NOTE: pass 'null' to allow this test advances time on the main thread.
     public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(/* test= */ null);
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
     @Mock
@@ -625,6 +622,7 @@
                 0);
     }
 
+    @DisableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
     @Test
     public void onScreenSizeAndDensityChanged_enabled_restoreSavedMagnifierWindow() {
         int newSmallestScreenWidthDp =
@@ -663,6 +661,50 @@
         assertTrue(params.height == (windowFrameSize + 2 * mirrorSurfaceMargin));
     }
 
+    @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
+    @Test
+    public void onScreenSizeAndDensityChanged_enabled_restoreSavedMagnifierIndexAndWindow() {
+        int newSmallestScreenWidthDp =
+                mContext.getResources().getConfiguration().smallestScreenWidthDp * 2;
+        int windowFrameSize = mResources.getDimensionPixelSize(
+                com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
+        Size preferredWindowSize = new Size(windowFrameSize, windowFrameSize);
+        mSharedPreferences
+                .edit()
+                .putString(String.valueOf(newSmallestScreenWidthDp),
+                        WindowMagnificationFrameSpec.serialize(
+                                WindowMagnificationSettings.MagnificationSize.CUSTOM,
+                                preferredWindowSize))
+                .commit();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.updateWindowMagnificationInternal(Float.NaN, Float.NaN,
+                    Float.NaN);
+        });
+
+        // Screen density and size change
+        mContext.getResources().getConfiguration().smallestScreenWidthDp = newSmallestScreenWidthDp;
+        final Rect testWindowBounds = new Rect(
+                mWindowManager.getCurrentWindowMetrics().getBounds());
+        testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+                testWindowBounds.right + 100, testWindowBounds.bottom + 100);
+        mWindowManager.setWindowBounds(testWindowBounds);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+        });
+
+        // wait for rect update
+        waitForIdleSync();
+        verify(mWindowMagnifierCallback).onWindowMagnifierBoundsRestored(
+                eq(mContext.getDisplayId()),
+                eq(WindowMagnificationSettings.MagnificationSize.CUSTOM));
+        ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+        final int mirrorSurfaceMargin = mResources.getDimensionPixelSize(
+                R.dimen.magnification_mirror_surface_margin);
+        // The width and height of the view include the magnification frame and the margins.
+        assertTrue(params.width == (windowFrameSize + 2 * mirrorSurfaceMargin));
+        assertTrue(params.height == (windowFrameSize + 2 * mirrorSurfaceMargin));
+    }
+
     @Test
     public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() {
         mInstrumentation.runOnMainSync(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
index ad9053a..944066fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.accessibility;
 
+import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -23,12 +25,15 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.util.Size;
 
 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.util.FakeSharedPreferences;
 
@@ -54,19 +59,59 @@
         mWindowMagnificationFrameSizePrefs = new WindowMagnificationFrameSizePrefs(mContext);
     }
 
+    @DisableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
     @Test
     public void saveSizeForCurrentDensity_getExpectedSize() {
         Size testSize = new Size(500, 500);
-        mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(testSize);
+        mWindowMagnificationFrameSizePrefs
+                .saveIndexAndSizeForCurrentDensity(MagnificationSize.CUSTOM, testSize);
 
         assertThat(mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity())
                 .isEqualTo(testSize);
     }
 
+    @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
+    @Test
+    public void saveSizeForCurrentDensity_validPreference_getExpectedSize() {
+        int testIndex = MagnificationSize.MEDIUM;
+        Size testSize = new Size(500, 500);
+        mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize);
+
+        assertThat(mWindowMagnificationFrameSizePrefs.getSizeForCurrentDensity())
+                .isEqualTo(testSize);
+    }
+
+    @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
+    @Test
+    public void saveSizeForCurrentDensity_validPreference_getExpectedIndex() {
+        int testIndex = MagnificationSize.MEDIUM;
+        Size testSize = new Size(500, 500);
+        mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize);
+
+        assertThat(mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity())
+                .isEqualTo(testIndex);
+    }
+
+    @EnableFlags(Flags.FLAG_SAVE_AND_RESTORE_MAGNIFICATION_SETTINGS_BUTTONS)
+    @Test
+    public void saveSizeForCurrentDensity_invalidPreference_getDefaultIndex() {
+        mSharedPreferences
+                .edit()
+                .putString(
+                        String.valueOf(
+                                mContext.getResources().getConfiguration().smallestScreenWidthDp),
+                        "100x200")
+                .commit();
+
+        assertThat(mWindowMagnificationFrameSizePrefs.getIndexForCurrentDensity())
+                .isEqualTo(MagnificationSize.DEFAULT);
+    }
+
     @Test
     public void saveSizeForCurrentDensity_containsPreferenceForCurrentDensity() {
+        int testIndex = MagnificationSize.MEDIUM;
         Size testSize = new Size(500, 500);
-        mWindowMagnificationFrameSizePrefs.saveSizeForCurrentDensity(testSize);
+        mWindowMagnificationFrameSizePrefs.saveIndexAndSizeForCurrentDensity(testIndex, testSize);
 
         assertThat(mWindowMagnificationFrameSizePrefs.isPreferenceSavedForCurrentDensity())
                 .isTrue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
new file mode 100644
index 0000000..791a26e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.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.systemui.accessibility
+
+import android.testing.AndroidTestingRunner
+import android.util.Size
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WindowMagnificationFrameSpecTest : SysuiTestCase() {
+
+    @Test
+    fun deserializeSpec_validSpec_expectedIndex() {
+        val targetIndex = MagnificationSize.LARGE
+        val targetSize = Size(100, 200)
+        val targetPreference = WindowMagnificationFrameSpec.serialize(targetIndex, targetSize)
+
+        assertThat(WindowMagnificationFrameSpec.deserialize(targetPreference).index)
+            .isEqualTo(targetIndex)
+    }
+
+    @Test
+    fun deserializeSpec_validSpec_expectedSize() {
+        val targetIndex = MagnificationSize.LARGE
+        val targetSize = Size(100, 200)
+        val targetPreference = WindowMagnificationFrameSpec.serialize(targetIndex, targetSize)
+
+        assertThat(WindowMagnificationFrameSpec.deserialize(targetPreference).size)
+            .isEqualTo(targetSize)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index 639b53b..5600b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -710,6 +710,35 @@
         environment.verifyLifecycleObserversUnregistered();
     }
 
+    @Test
+    public void testLastSessionPop_createsNewInputSession() {
+        final TouchHandler touchHandler = createTouchHandler();
+
+        final TouchHandler.TouchSession.Callback callback =
+                Mockito.mock(TouchHandler.TouchSession.Callback.class);
+
+        final Environment environment = new Environment(Stream.of(touchHandler)
+                .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+
+        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
+        environment.publishInputEvent(initialEvent);
+
+        final TouchHandler.TouchSession session = captureSession(touchHandler);
+        session.registerCallback(callback);
+
+        // Clear invocations on input session and factory.
+        clearInvocations(environment.mInputFactory);
+        clearInvocations(environment.mInputSession);
+
+        // Pop only active touch session.
+        session.pop();
+        environment.executeAll();
+
+        // Verify that input session disposed and new session requested from factory.
+        verify(environment.mInputSession).dispose();
+        verify(environment.mInputFactory).create(any(), any(), any(), anyBoolean());
+    }
+
     private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) {
         final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
                 GestureDetector.OnGestureListener.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index d940fc9..7a4bbfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -36,7 +36,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -77,9 +76,6 @@
 
         when(mBatteryMeterView.getContext()).thenReturn(mContext);
         when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources());
-
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, false);
     }
 
     @Test
@@ -136,26 +132,6 @@
         verify(mTunerService, never()).addTunable(any(), any());
     }
 
-    @Test
-    public void shieldFlagDisabled_viewNotified() {
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, false);
-
-        initController();
-
-        verify(mBatteryMeterView).setDisplayShieldEnabled(false);
-    }
-
-    @Test
-    public void shieldFlagEnabled_viewNotified() {
-        mContext.getOrCreateTestableResources().addOverride(
-                R.bool.flag_battery_shield_icon, true);
-
-        initController();
-
-        verify(mBatteryMeterView).setDisplayShieldEnabled(true);
-    }
-
     private void initController() {
         mController = new BatteryMeterViewController(
                 mBatteryMeterView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
index 2bd0976..2aa33a17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
@@ -22,9 +22,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.flags.Flags.FLAG_NEW_STATUS_BAR_ICONS
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -67,9 +67,8 @@
     fun contentDescription_unknown() {
         mBatteryMeterView.onBatteryUnknownStateChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_unknown)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_unknown))
     }
 
     @Test
@@ -80,11 +79,10 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(
-                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
-                )
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+            )
     }
 
     @Test
@@ -96,13 +94,14 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_charging_paused_with_estimate,
-                        17,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_charging_paused_with_estimate,
+                    17,
+                    ESTIMATE,
                 )
-        )
+            )
     }
 
     @Test
@@ -110,27 +109,24 @@
         mBatteryMeterView.onBatteryLevelChanged(90, false)
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
     }
 
     @Test
     fun contentDescription_charging() {
         mBatteryMeterView.onBatteryLevelChanged(45, true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 45))
     }
 
     @Test
     fun contentDescription_notCharging() {
         mBatteryMeterView.onBatteryLevelChanged(45, false)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
     }
 
     @Test
@@ -138,9 +134,8 @@
         mBatteryMeterView.onBatteryLevelChanged(45, true)
         mBatteryMeterView.onIsIncompatibleChargingChanged(true)
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 45)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 45))
     }
 
     @Test
@@ -152,19 +147,17 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(
-                        R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
-                )
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
+            )
 
         // Update the show mode from estimate to percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
 
         assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%")
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 15)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
     }
 
     @Test
@@ -176,19 +169,17 @@
 
         mBatteryMeterView.updatePercentText()
 
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-            context.getString(
-                R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
+                context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE)
             )
-        )
 
         // Update the show mode from estimate to percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
 
         assertThat(mBatteryMeterView.batteryPercentView).isNull()
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-            context.getString(R.string.accessibility_battery_level, 15)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 15))
         assertThat(mBatteryMeterView.unifiedBatteryState.showPercent).isTrue()
     }
 
@@ -225,49 +216,47 @@
         // BatteryDefender
         mBatteryMeterView.onBatteryLevelChanged(90, false)
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging_paused, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90))
 
         // BatteryDefender & estimate
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
         mBatteryMeterView.setBatteryEstimateFetcher(Fetcher())
         mBatteryMeterView.updatePercentText()
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_charging_paused_with_estimate,
-                        90,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_charging_paused_with_estimate,
+                    90,
+                    ESTIMATE,
                 )
-        )
+            )
 
         // Just estimate
         mBatteryMeterView.onIsBatteryDefenderChanged(false)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(
                 context.getString(
-                        R.string.accessibility_battery_level_with_estimate,
-                        90,
-                        ESTIMATE,
+                    R.string.accessibility_battery_level_with_estimate,
+                    90,
+                    ESTIMATE,
                 )
-        )
+            )
 
         // Just percent
         mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level, 90))
 
         // Charging
         mBatteryMeterView.onBatteryLevelChanged(90, true)
-        assertThat(mBatteryMeterView.contentDescription).isEqualTo(
-                context.getString(R.string.accessibility_battery_level_charging, 90)
-        )
+        assertThat(mBatteryMeterView.contentDescription)
+            .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 90))
     }
 
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
         val drawable = getBatteryDrawable()
 
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
@@ -278,8 +267,6 @@
     @Test
     @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
-
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
         assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNotNull()
@@ -288,7 +275,6 @@
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
         val drawable = getBatteryDrawable()
 
         // Start as true
@@ -303,8 +289,6 @@
     @Test
     @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(true)
-
         // Start as true
         mBatteryMeterView.onIsBatteryDefenderChanged(true)
 
@@ -316,27 +300,6 @@
 
     @Test
     @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
-    fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOff() {
-        mBatteryMeterView.setDisplayShieldEnabled(false)
-        val drawable = getBatteryDrawable()
-
-        mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
-        assertThat(drawable.displayShield).isFalse()
-    }
-
-    @Test
-    @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS)
-    fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOn() {
-        mBatteryMeterView.setDisplayShieldEnabled(false)
-
-        mBatteryMeterView.onIsBatteryDefenderChanged(true)
-
-        assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNull()
-    }
-
-    @Test
-    @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS)
     fun isIncompatibleChargingChanged_true_drawableGetsChargingFalse_flagOff() {
         mBatteryMeterView.onBatteryLevelChanged(45, true)
         val drawable = getBatteryDrawable()
@@ -381,8 +344,8 @@
     }
 
     private fun getBatteryDrawable(): AccessorizedBatteryDrawable {
-        return (mBatteryMeterView.getChildAt(0) as ImageView)
-                .drawable as AccessorizedBatteryDrawable
+        return (mBatteryMeterView.getChildAt(0) as ImageView).drawable
+            as AccessorizedBatteryDrawable
     }
 
     private class Fetcher : BatteryEstimateFetcher {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index 6dc4b10..bbff539 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -18,6 +18,10 @@
 
 import android.graphics.Point
 import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.ComponentInfoInternal
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.biometrics.SensorProperties
+import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.testing.TestableLooper.RunWithLooper
 import android.util.DisplayMetrics
@@ -43,6 +47,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.leak.RotationUtils
 import com.android.systemui.util.mockito.any
+import javax.inject.Provider
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.After
 import org.junit.Assert.assertFalse
@@ -62,8 +67,6 @@
 import org.mockito.MockitoAnnotations
 import org.mockito.MockitoSession
 import org.mockito.quality.Strictness
-import javax.inject.Provider
-
 
 @ExperimentalCoroutinesApi
 @SmallTest
@@ -79,35 +82,28 @@
     @Mock private lateinit var authController: AuthController
     @Mock private lateinit var authRippleInteractor: AuthRippleInteractor
     @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock
-    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
-    @Mock
-    private lateinit var notificationShadeWindowController: NotificationShadeWindowController
-    @Mock
-    private lateinit var biometricUnlockController: BiometricUnlockController
-    @Mock
-    private lateinit var udfpsControllerProvider: Provider<UdfpsController>
-    @Mock
-    private lateinit var udfpsController: UdfpsController
-    @Mock
-    private lateinit var statusBarStateController: StatusBarStateController
-    @Mock
-    private lateinit var lightRevealScrim: LightRevealScrim
-    @Mock
-    private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+    @Mock private lateinit var biometricUnlockController: BiometricUnlockController
+    @Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController>
+    @Mock private lateinit var udfpsController: UdfpsController
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var lightRevealScrim: LightRevealScrim
+    @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
 
     private val facePropertyRepository = FakeFacePropertyRepository()
     private val displayMetrics = DisplayMetrics()
 
     @Captor
     private lateinit var biometricUnlockListener:
-            ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
+        ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
 
     @Before
     fun setUp() {
         mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         MockitoAnnotations.initMocks(this)
-        staticMockSession = mockitoSession()
+        staticMockSession =
+            mockitoSession()
                 .mockStatic(RotationUtils::class.java)
                 .strictness(Strictness.LENIENT)
                 .startMocking()
@@ -116,25 +112,26 @@
         `when`(authController.udfpsProps).thenReturn(listOf(fpSensorProp))
         `when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
 
-        controller = AuthRippleController(
-            context,
-            authController,
-            configurationController,
-            keyguardUpdateMonitor,
-            keyguardStateController,
-            wakefulnessLifecycle,
-            commandRegistry,
-            notificationShadeWindowController,
-            udfpsControllerProvider,
-            statusBarStateController,
-            displayMetrics,
-            KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
-            biometricUnlockController,
-            lightRevealScrim,
-            authRippleInteractor,
-            facePropertyRepository,
-            rippleView,
-        )
+        controller =
+            AuthRippleController(
+                context,
+                authController,
+                configurationController,
+                keyguardUpdateMonitor,
+                keyguardStateController,
+                wakefulnessLifecycle,
+                commandRegistry,
+                notificationShadeWindowController,
+                udfpsControllerProvider,
+                statusBarStateController,
+                displayMetrics,
+                KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
+                biometricUnlockController,
+                lightRevealScrim,
+                authRippleInteractor,
+                facePropertyRepository,
+                rippleView,
+            )
         controller.init()
     }
 
@@ -150,13 +147,18 @@
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
         `when`(keyguardStateController.isShowing).thenReturn(true)
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
+        `when`(
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                    eq(BiometricSourceType.FINGERPRINT)
+                )
+            )
+            .thenReturn(true)
 
         // WHEN fingerprint authenticated
         verify(biometricUnlockController).addListener(biometricUnlockListener.capture())
-        biometricUnlockListener.value
-                .onBiometricUnlockedWithKeyguardDismissal(BiometricSourceType.FINGERPRINT)
+        biometricUnlockListener.value.onBiometricUnlockedWithKeyguardDismissal(
+            BiometricSourceType.FINGERPRINT
+        )
 
         // THEN update sensor location and show ripple
         verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f)
@@ -169,8 +171,12 @@
         val fpsLocation = Point(5, 5)
         `when`(authController.udfpsLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                eq(BiometricSourceType.FINGERPRINT))).thenReturn(true)
+        `when`(
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                    eq(BiometricSourceType.FINGERPRINT)
+                )
+            )
+            .thenReturn(true)
 
         // WHEN keyguard is NOT showing & fingerprint authenticated
         `when`(keyguardStateController.isShowing).thenReturn(false)
@@ -179,7 +185,8 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */)
+            false /* isStrongBiometric */
+        )
 
         // THEN no ripple
         verify(rippleView, never()).startUnlockedRipple(any())
@@ -194,14 +201,19 @@
         `when`(keyguardStateController.isShowing).thenReturn(true)
 
         // WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                eq(BiometricSourceType.FINGERPRINT))).thenReturn(false)
+        `when`(
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                    eq(BiometricSourceType.FINGERPRINT)
+                )
+            )
+            .thenReturn(false)
         val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
         verify(keyguardUpdateMonitor).registerCallback(captor.capture())
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */)
+            false /* isStrongBiometric */
+        )
 
         // THEN no ripple
         verify(rippleView, never()).startUnlockedRipple(any())
@@ -218,7 +230,8 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FACE /* type */,
-            false /* isStrongBiometric */)
+            false /* isStrongBiometric */
+        )
         verify(rippleView, never()).startUnlockedRipple(any())
     }
 
@@ -233,18 +246,17 @@
         captor.value.onBiometricAuthenticated(
             0 /* userId */,
             BiometricSourceType.FINGERPRINT /* type */,
-            false /* isStrongBiometric */)
+            false /* isStrongBiometric */
+        )
         verify(rippleView, never()).startUnlockedRipple(any())
     }
 
     @Test
     fun registersAndDeregisters() {
         controller.onViewAttached()
-        val captor = ArgumentCaptor
-            .forClass(KeyguardStateController.Callback::class.java)
+        val captor = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java)
         verify(keyguardStateController).addCallback(captor.capture())
-        val captor2 = ArgumentCaptor
-            .forClass(WakefulnessLifecycle.Observer::class.java)
+        val captor2 = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java)
         verify(wakefulnessLifecycle).addObserver(captor2.capture())
         controller.onViewDetached()
         verify(keyguardStateController).removeCallback(any())
@@ -259,17 +271,25 @@
         `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation)
         controller.onViewAttached()
         `when`(keyguardStateController.isShowing).thenReturn(true)
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                BiometricSourceType.FINGERPRINT)).thenReturn(true)
+        `when`(
+                keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                    BiometricSourceType.FINGERPRINT
+                )
+            )
+            .thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
 
         controller.showUnlockRipple(BiometricSourceType.FINGERPRINT)
-        assertTrue("reveal didn't start on keyguardFadingAway",
-            controller.startLightRevealScrimOnKeyguardFadingAway)
+        assertTrue(
+            "reveal didn't start on keyguardFadingAway",
+            controller.startLightRevealScrimOnKeyguardFadingAway
+        )
         `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
         controller.onKeyguardFadingAwayChanged()
-        assertFalse("reveal triggers multiple times",
-            controller.startLightRevealScrimOnKeyguardFadingAway)
+        assertFalse(
+            "reveal triggers multiple times",
+            controller.startLightRevealScrimOnKeyguardFadingAway
+        )
     }
 
     @Test
@@ -282,23 +302,27 @@
         `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
         `when`(authController.isUdfpsFingerDown).thenReturn(true)
-        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                eq(BiometricSourceType.FACE))).thenReturn(true)
+        `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(eq(BiometricSourceType.FACE)))
+            .thenReturn(true)
 
         controller.showUnlockRipple(BiometricSourceType.FACE)
-        assertTrue("reveal didn't start on keyguardFadingAway",
-                controller.startLightRevealScrimOnKeyguardFadingAway)
+        assertTrue(
+            "reveal didn't start on keyguardFadingAway",
+            controller.startLightRevealScrimOnKeyguardFadingAway
+        )
         `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true)
         controller.onKeyguardFadingAwayChanged()
-        assertFalse("reveal triggers multiple times",
-                controller.startLightRevealScrimOnKeyguardFadingAway)
+        assertFalse(
+            "reveal triggers multiple times",
+            controller.startLightRevealScrimOnKeyguardFadingAway
+        )
     }
 
     @Test
     fun testUpdateRippleColor() {
         controller.onViewAttached()
-        val captor = ArgumentCaptor
-            .forClass(ConfigurationController.ConfigurationListener::class.java)
+        val captor =
+            ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
         verify(configurationController).addCallback(captor.capture())
 
         reset(rippleView)
@@ -333,6 +357,40 @@
     }
 
     @Test
+    fun testUltrasonicUdfps_onFingerDown_runningForDeviceEntry_doNotShowDwellRipple() {
+        // GIVEN UDFPS is ultrasonic
+        `when`(authController.udfpsProps)
+            .thenReturn(
+                listOf(
+                    FingerprintSensorPropertiesInternal(
+                        0 /* sensorId */,
+                        SensorProperties.STRENGTH_STRONG,
+                        5 /* maxEnrollmentsPerUser */,
+                        listOf<ComponentInfoInternal>(),
+                        FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC,
+                        false /* halControlsIllumination */,
+                        true /* resetLockoutRequiresHardwareAuthToken */,
+                        listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
+                    )
+                )
+            )
+
+        // GIVEN fingerprint detection is running on keyguard
+        `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true)
+
+        // GIVEN view is already attached
+        controller.onViewAttached()
+        val captor = ArgumentCaptor.forClass(UdfpsController.Callback::class.java)
+        verify(udfpsController).addCallback(captor.capture())
+
+        // WHEN finger is down
+        captor.value.onFingerDown()
+
+        // THEN never show dwell ripple
+        verify(rippleView, never()).startDwellRipple(false)
+    }
+
+    @Test
     fun testUdfps_onFingerDown_notDeviceEntry_doesNotShowDwellRipple() {
         // GIVEN fingerprint detection is NOT running on keyguard
         `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 677d1fd..6dcea14 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -449,96 +449,115 @@
     }
 
     @Test
-    fun shows_authenticated_no_errors_no_confirmation_required() = runGenericTest {
+    fun shows_error_to_unlock_or_success() {
+        // Face-only auth does not use error -> unlock or error -> success assets
+        if (testCase.isFingerprintOnly || testCase.isCoex) {
+            runGenericTest {
+                // Distinct asset for error -> success only applicable for fingerprint-only /
+                // explicit co-ex auth
+                val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+                val iconContentDescriptionId by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
+                val shouldAnimateIconView by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
+
+                var forceExplicitFlow =
+                    testCase.isCoex && testCase.confirmationRequested ||
+                        testCase.authenticatedByFingerprint
+                if (forceExplicitFlow) {
+                    kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
+                }
+                verifyIconSize(forceExplicitFlow)
+
+                kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true)
+                kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true)
+
+                kosmos.promptViewModel.showAuthenticated(
+                    modality = testCase.authenticatedModality,
+                    dismissAfterDelay = DELAY
+                )
+
+                // TODO(b/350121748): SFPS test cases to be added after SFPS assets update
+                if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) {
+                    // Non-SFPS (UDFPS / rear-FPS) test cases
+                    // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint
+                    if (testCase.authenticatedByFingerprint) {
+                        assertThat(iconAsset)
+                            .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie)
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                    } else { //  co-ex, authenticated by face
+                        assertThat(iconAsset)
+                            .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie)
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+
+                        // Confirm authentication
+                        kosmos.promptViewModel.confirmAuthenticated()
+
+                        assertThat(iconAsset)
+                            .isEqualTo(
+                                R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
+                            )
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun shows_authenticated_no_errors_no_confirmation_required() {
         if (!testCase.confirmationRequested) {
-            val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
-            val iconOverlayAsset by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
-            val iconContentDescriptionId by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
-            val shouldAnimateIconView by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
-            val shouldAnimateIconOverlay by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
-            verifyIconSize()
+            runGenericTest {
+                val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+                val iconOverlayAsset by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
+                val iconContentDescriptionId by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
+                val shouldAnimateIconView by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
+                val shouldAnimateIconOverlay by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
+                verifyIconSize()
 
-            kosmos.promptViewModel.showAuthenticated(
-                modality = testCase.authenticatedModality,
-                dismissAfterDelay = DELAY
-            )
+                kosmos.promptViewModel.showAuthenticated(
+                    modality = testCase.authenticatedModality,
+                    dismissAfterDelay = DELAY
+                )
 
-            if (testCase.isFingerprintOnly) {
-                // Fingerprint icon asset assertions
-                if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
-                    assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset())
-                    assertThat(iconOverlayAsset)
-                        .isEqualTo(R.raw.biometricprompt_symbol_fingerprint_to_success_landscape)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
-                    assertThat(shouldAnimateIconView).isEqualTo(true)
-                    assertThat(shouldAnimateIconOverlay).isEqualTo(true)
-                } else {
-                    assertThat(iconAsset)
-                        .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie)
+                if (testCase.isFingerprintOnly) {
+                    // Fingerprint icon asset assertions
+                    if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
+                        assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset())
+                        assertThat(iconOverlayAsset)
+                            .isEqualTo(
+                                R.raw.biometricprompt_symbol_fingerprint_to_success_landscape
+                            )
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                        assertThat(shouldAnimateIconOverlay).isEqualTo(true)
+                    } else {
+                        assertThat(iconAsset)
+                            .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie)
+                        assertThat(iconOverlayAsset).isEqualTo(-1)
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                        assertThat(shouldAnimateIconOverlay).isEqualTo(false)
+                    }
+                } else if (testCase.isFaceOnly || testCase.isCoex) {
+                    // Face icon asset assertions
+                    // If co-ex, use implicit flow (explicit flow always requires confirmation)
+                    assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
                     assertThat(iconOverlayAsset).isEqualTo(-1)
                     assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
-                    assertThat(shouldAnimateIconView).isEqualTo(true)
-                    assertThat(shouldAnimateIconOverlay).isEqualTo(false)
-                }
-            } else if (testCase.isFaceOnly || testCase.isCoex) {
-                // Face icon asset assertions
-                // If co-ex, use implicit flow (explicit flow always requires confirmation)
-                assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
-                assertThat(iconOverlayAsset).isEqualTo(-1)
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
-                assertThat(shouldAnimateIconView).isEqualTo(true)
-                assertThat(shouldAnimateIconOverlay).isEqualTo(false)
-            }
-        }
-    }
-
-    @Test
-    fun shows_pending_confirmation() = runGenericTest {
-        if (
-            (testCase.isFaceOnly || testCase.isCoex) &&
-                testCase.authenticatedByFace &&
-                testCase.confirmationRequested
-        ) {
-            val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
-            val iconOverlayAsset by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
-            val iconContentDescriptionId by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
-            val shouldAnimateIconView by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
-            val shouldAnimateIconOverlay by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
-
-            val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
-            verifyIconSize(forceExplicitFlow = forceExplicitFlow)
-
-            kosmos.promptViewModel.showAuthenticated(
-                modality = testCase.authenticatedModality,
-                dismissAfterDelay = DELAY
-            )
-
-            if (testCase.isFaceOnly) {
-                assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark)
-                assertThat(iconOverlayAsset).isEqualTo(-1)
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
-                assertThat(shouldAnimateIconView).isEqualTo(true)
-                assertThat(shouldAnimateIconOverlay).isEqualTo(false)
-            } else if (testCase.isCoex) { // explicit flow, confirmation requested
-                // TODO: Update when SFPS co-ex is implemented
-                if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) {
-                    assertThat(iconAsset)
-                        .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
-                    assertThat(iconOverlayAsset).isEqualTo(-1)
-                    assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                        .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                     assertThat(shouldAnimateIconOverlay).isEqualTo(false)
                 }
@@ -547,51 +566,96 @@
     }
 
     @Test
-    fun shows_authenticated_explicitly_confirmed_iconUpdate() = runGenericTest {
-        if (
-            (testCase.isFaceOnly || testCase.isCoex) &&
-                testCase.authenticatedByFace &&
-                testCase.confirmationRequested
-        ) {
-            val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
-            val iconOverlayAsset by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
-            val iconContentDescriptionId by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
-            val shouldAnimateIconView by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
-            val shouldAnimateIconOverlay by
-                collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
-            val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
-            verifyIconSize(forceExplicitFlow = forceExplicitFlow)
+    fun shows_pending_confirmation() {
+        if (testCase.authenticatedByFace && testCase.confirmationRequested) {
+            runGenericTest {
+                val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+                val iconOverlayAsset by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
+                val iconContentDescriptionId by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
+                val shouldAnimateIconView by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
+                val shouldAnimateIconOverlay by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
 
-            kosmos.promptViewModel.showAuthenticated(
-                modality = testCase.authenticatedModality,
-                dismissAfterDelay = DELAY
-            )
+                val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
+                verifyIconSize(forceExplicitFlow = forceExplicitFlow)
 
-            kosmos.promptViewModel.confirmAuthenticated()
+                kosmos.promptViewModel.showAuthenticated(
+                    modality = testCase.authenticatedModality,
+                    dismissAfterDelay = DELAY
+                )
 
-            if (testCase.isFaceOnly) {
-                assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
-                assertThat(iconOverlayAsset).isEqualTo(-1)
-                assertThat(iconContentDescriptionId)
-                    .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
-                assertThat(shouldAnimateIconView).isEqualTo(true)
-                assertThat(shouldAnimateIconOverlay).isEqualTo(false)
-            }
-
-            // explicit flow because confirmation requested
-            if (testCase.isCoex) {
-                // TODO: Update when SFPS co-ex is implemented
-                if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) {
-                    assertThat(iconAsset)
-                        .isEqualTo(R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie)
+                if (testCase.isFaceOnly) {
+                    assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark)
                     assertThat(iconOverlayAsset).isEqualTo(-1)
                     assertThat(iconContentDescriptionId)
-                        .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
                     assertThat(shouldAnimateIconOverlay).isEqualTo(false)
+                } else if (testCase.isCoex) { // explicit flow, confirmation requested
+                    // TODO: Update when SFPS co-ex is implemented
+                    if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) {
+                        assertThat(iconAsset)
+                            .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie)
+                        assertThat(iconOverlayAsset).isEqualTo(-1)
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                        assertThat(shouldAnimateIconOverlay).isEqualTo(false)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun shows_authenticated_explicitly_confirmed() {
+        if (testCase.authenticatedByFace && testCase.confirmationRequested) {
+            runGenericTest {
+                val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+                val iconOverlayAsset by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset)
+                val iconContentDescriptionId by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId)
+                val shouldAnimateIconView by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView)
+                val shouldAnimateIconOverlay by
+                    collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay)
+                val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested
+                verifyIconSize(forceExplicitFlow = forceExplicitFlow)
+
+                kosmos.promptViewModel.showAuthenticated(
+                    modality = testCase.authenticatedModality,
+                    dismissAfterDelay = DELAY
+                )
+
+                kosmos.promptViewModel.confirmAuthenticated()
+
+                if (testCase.isFaceOnly) {
+                    assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark)
+                    assertThat(iconOverlayAsset).isEqualTo(-1)
+                    assertThat(iconContentDescriptionId)
+                        .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
+                    assertThat(shouldAnimateIconView).isEqualTo(true)
+                    assertThat(shouldAnimateIconOverlay).isEqualTo(false)
+                }
+
+                // explicit flow because confirmation requested
+                if (testCase.isCoex) {
+                    // TODO: Update when SFPS co-ex is implemented
+                    if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) {
+                        assertThat(iconAsset)
+                            .isEqualTo(
+                                R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie
+                            )
+                        assertThat(iconOverlayAsset).isEqualTo(-1)
+                        assertThat(iconContentDescriptionId)
+                            .isEqualTo(R.string.fingerprint_dialog_touch_sensor)
+                        assertThat(shouldAnimateIconView).isEqualTo(true)
+                        assertThat(shouldAnimateIconOverlay).isEqualTo(false)
+                    }
                 }
             }
         }
@@ -700,58 +764,68 @@
     }
 
     @Test
-    fun sfpsIconUpdates_onFoldConfigurationChanged() = runGenericTest {
+    fun sfpsIconUpdates_onFoldConfigurationChanged() {
         if (
             testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON &&
                 !testCase.isInRearDisplayMode
         ) {
-            val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+            runGenericTest {
+                val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
 
-            kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getFoldedConfiguration())
-            val foldedIcon = currentIcon
+                kosmos.promptViewModel.iconViewModel.onConfigurationChanged(
+                    getFoldedConfiguration()
+                )
+                val foldedIcon = currentIcon
 
-            kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getUnfoldedConfiguration())
-            val unfoldedIcon = currentIcon
+                kosmos.promptViewModel.iconViewModel.onConfigurationChanged(
+                    getUnfoldedConfiguration()
+                )
+                val unfoldedIcon = currentIcon
 
-            assertThat(foldedIcon).isNotEqualTo(unfoldedIcon)
+                assertThat(foldedIcon).isNotEqualTo(unfoldedIcon)
+            }
         }
     }
 
     @Test
-    fun sfpsIconUpdates_onRotation() = runGenericTest {
+    fun sfpsIconUpdates_onRotation() {
         if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
-            val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+            runGenericTest {
+                val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
 
-            kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
-            val iconRotation0 = currentIcon
+                kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
+                val iconRotation0 = currentIcon
 
-            kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
-            val iconRotation90 = currentIcon
+                kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
+                val iconRotation90 = currentIcon
 
-            kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
-            val iconRotation180 = currentIcon
+                kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
+                val iconRotation180 = currentIcon
 
-            kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
-            val iconRotation270 = currentIcon
+                kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
+                val iconRotation270 = currentIcon
 
-            assertThat(iconRotation0).isEqualTo(iconRotation180)
-            assertThat(iconRotation0).isNotEqualTo(iconRotation90)
-            assertThat(iconRotation0).isNotEqualTo(iconRotation270)
+                assertThat(iconRotation0).isEqualTo(iconRotation180)
+                assertThat(iconRotation0).isNotEqualTo(iconRotation90)
+                assertThat(iconRotation0).isNotEqualTo(iconRotation270)
+            }
         }
     }
 
     @Test
-    fun sfpsIconUpdates_onRearDisplayMode() = runGenericTest {
+    fun sfpsIconUpdates_onRearDisplayMode() {
         if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) {
-            val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
+            runGenericTest {
+                val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset)
 
-            kosmos.displayStateRepository.setIsInRearDisplayMode(false)
-            val iconNotRearDisplayMode = currentIcon
+                kosmos.displayStateRepository.setIsInRearDisplayMode(false)
+                val iconNotRearDisplayMode = currentIcon
 
-            kosmos.displayStateRepository.setIsInRearDisplayMode(true)
-            val iconRearDisplayMode = currentIcon
+                kosmos.displayStateRepository.setIsInRearDisplayMode(true)
+                val iconRearDisplayMode = currentIcon
 
-            assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode)
+                assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode)
+            }
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
index 10b3ce3..0489d81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
@@ -89,7 +89,8 @@
         verify(taskView).startActivity(any(), any(), capture(optionsCaptor), any())
 
         assertThat(optionsCaptor.value.pendingIntentBackgroundActivityStartMode)
-            .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+            .isAnyOf(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED,
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
         assertThat(optionsCaptor.value.isPendingIntentBackgroundActivityLaunchAllowedByPermission)
             .isTrue()
         assertThat(optionsCaptor.value.taskAlwaysOnTop).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.kt
new file mode 100644
index 0000000..4d112e9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/CurrentAppShortcutsSourceTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.source
+
+import android.view.KeyboardShortcutGroup
+import android.view.WindowManager
+import android.view.mockWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CurrentAppShortcutsSourceTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val mockWindowManager = kosmos.mockWindowManager
+    private val source = CurrentAppShortcutsSource(mockWindowManager)
+
+    private var shortcutGroups: List<KeyboardShortcutGroup>? = null
+
+    @Before
+    fun setUp() {
+        whenever(mockWindowManager.requestAppKeyboardShortcuts(any(), any())).thenAnswer {
+            val receiver = it.arguments[0] as WindowManager.KeyboardShortcutsReceiver
+            receiver.onKeyboardShortcutsReceived(shortcutGroups)
+        }
+    }
+
+    @Test
+    fun shortcutGroups_wmReturnsNullList_returnsEmptyList() =
+        testScope.runTest {
+            shortcutGroups = null
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).isEmpty()
+        }
+
+    @Test
+    fun shortcutGroups_wmReturnsEmptyList_returnsEmptyList() =
+        testScope.runTest {
+            shortcutGroups = emptyList()
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).isEmpty()
+        }
+
+    @Test
+    fun shortcutGroups_wmReturnsGroups_returnsWmGroups() =
+        testScope.runTest {
+            shortcutGroups =
+                listOf(
+                    KeyboardShortcutGroup("wm ime group 1"),
+                    KeyboardShortcutGroup("wm ime group 2"),
+                    KeyboardShortcutGroup("wm ime group 3"),
+                )
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).hasSize(3)
+        }
+
+    companion object {
+        private const val TEST_DEVICE_ID = 9876
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt
new file mode 100644
index 0000000..715d907
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/source/InputShortcutsSourceTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.data.source
+
+import android.content.res.mainResources
+import android.view.KeyboardShortcutGroup
+import android.view.WindowManager.KeyboardShortcutsReceiver
+import android.view.mockWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InputShortcutsSourceTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val mockWindowManager = kosmos.mockWindowManager
+    private val source = InputShortcutsSource(kosmos.mainResources, mockWindowManager)
+
+    private var wmImeShortcutGroups: List<KeyboardShortcutGroup>? = null
+
+    @Before
+    fun setUp() {
+        whenever(mockWindowManager.requestImeKeyboardShortcuts(any(), any())).thenAnswer {
+            val receiver = it.arguments[0] as KeyboardShortcutsReceiver
+            receiver.onKeyboardShortcutsReceived(wmImeShortcutGroups)
+        }
+    }
+
+    @Test
+    fun shortcutGroups_wmReturnsNullList_returnsSingleGroup() =
+        testScope.runTest {
+            wmImeShortcutGroups = null
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).hasSize(1)
+        }
+
+    @Test
+    fun shortcutGroups_wmReturnsEmptyList_returnsSingleGroup() =
+        testScope.runTest {
+            wmImeShortcutGroups = emptyList()
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).hasSize(1)
+        }
+
+    @Test
+    fun shortcutGroups_wmReturnsGroups_returnsWmGroupsPlusOne() =
+        testScope.runTest {
+            wmImeShortcutGroups =
+                listOf(
+                    KeyboardShortcutGroup("wm ime group 1"),
+                    KeyboardShortcutGroup("wm ime group 2"),
+                    KeyboardShortcutGroup("wm ime group 3"),
+                )
+
+            val groups = source.shortcutGroups(TEST_DEVICE_ID)
+
+            assertThat(groups).hasSize(4)
+        }
+
+    companion object {
+        private const val TEST_DEVICE_ID = 1234
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 693a877..7cc9185 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.graphics.Point
+import android.platform.test.annotations.DisableFlags
 import android.view.WindowManager
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
@@ -106,8 +107,8 @@
     }
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun addViewsConditionally_migrateFlagOff() {
-        mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
         mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
         mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         val constraintLayout = ConstraintLayout(context, null)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index 201ee88..1c99eff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.ui.view.layout.sections
 
+import android.platform.test.annotations.EnableFlags
 import android.view.View
 import android.widget.LinearLayout
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -48,6 +49,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
+@EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 class SmartspaceSectionTest : SysuiTestCase() {
     private lateinit var underTest: SmartspaceSection
     @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
@@ -70,7 +72,6 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
         underTest =
             SmartspaceSection(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 4f4aac4..3b96be4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -152,7 +152,6 @@
         val featureFlags =
             FakeFeatureFlags().apply {
                 set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
-                set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
             }
 
         val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 9fb1aa7..e89abf6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -195,7 +195,6 @@
         val featureFlags =
             FakeFeatureFlags().apply {
                 set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
-                set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false)
             }
 
         val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
index d183c73..7dd8028 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
@@ -47,13 +47,7 @@
     private lateinit var dialog: AlertDialog
 
     private val flags = mock<FeatureFlagsClassic>()
-    private val onStartRecordingClicked = mock<Runnable>()
-    private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
-
-    private val mediaProjectionConfig: MediaProjectionConfig =
-        MediaProjectionConfig.createConfigForDefaultDisplay()
-    private val appName: String = "testApp"
-    private val hostUid: Int = 12345
+    private val appName = "Test App"
 
     private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
     private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
@@ -73,32 +67,8 @@
     }
 
     @Test
-    fun showDialog_forceShowPartialScreenShareFalse() {
-        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
-        // overrideDisableSingleAppOption = false
-        val overrideDisableSingleAppOption = false
-        setUpAndShowDialog(overrideDisableSingleAppOption)
-
-        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
-        val secondOptionText =
-            spinner.adapter
-                .getDropDownView(1, null, spinner)
-                .findViewById<TextView>(android.R.id.text2)
-                ?.text
-
-        // check that the first option is full screen and enabled
-        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
-
-        // check that the second option is single app and disabled
-        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText)
-    }
-
-    @Test
-    fun showDialog_forceShowPartialScreenShareTrue() {
-        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
-        // overrideDisableSingleAppOption = true
-        val overrideDisableSingleAppOption = true
-        setUpAndShowDialog(overrideDisableSingleAppOption)
+    fun showDefaultDialog() {
+        setUpAndShowDialog()
 
         val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
         val secondOptionText =
@@ -114,17 +84,84 @@
         assertEquals(context.getString(resIdFullScreen), secondOptionText)
     }
 
-    private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) {
+    @Test
+    fun showDialog_disableSingleApp() {
+        setUpAndShowDialog(
+            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
+        )
+
+        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+        val secondOptionWarningText =
+            spinner.adapter
+                .getDropDownView(1, null, spinner)
+                .findViewById<TextView>(android.R.id.text2)
+                ?.text
+
+        // check that the first option is full screen and enabled
+        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
+
+        // check that the second option is single app and disabled
+        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
+    }
+
+    @Test
+    fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() {
+        setUpAndShowDialog(
+            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
+            overrideDisableSingleAppOption = true
+        )
+
+        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+        val secondOptionText =
+            spinner.adapter
+                .getDropDownView(1, null, spinner)
+                .findViewById<TextView>(android.R.id.text1)
+                ?.text
+
+        // check that the first option is single app and enabled
+        assertEquals(context.getString(resIdSingleApp), spinner.selectedItem)
+
+        // check that the second option is full screen and enabled
+        assertEquals(context.getString(resIdFullScreen), secondOptionText)
+    }
+
+    @Test
+    fun showDialog_disableSingleApp_hasCastingCapabilities() {
+        setUpAndShowDialog(
+            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
+            hasCastingCapabilities = true
+        )
+
+        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+        val secondOptionWarningText =
+            spinner.adapter
+                .getDropDownView(1, null, spinner)
+                .findViewById<TextView>(android.R.id.text2)
+                ?.text
+
+        // check that the first option is full screen and enabled
+        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
+
+        // check that the second option is single app and disabled
+        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
+    }
+
+    private fun setUpAndShowDialog(
+        mediaProjectionConfig: MediaProjectionConfig? = null,
+        overrideDisableSingleAppOption: Boolean = false,
+        hasCastingCapabilities: Boolean = false,
+    ) {
         val delegate =
             MediaProjectionPermissionDialogDelegate(
                 context,
                 mediaProjectionConfig,
-                {},
-                onStartRecordingClicked,
+                onStartRecordingClicked = {},
+                onCancelClicked = {},
+                hasCastingCapabilities,
                 appName,
                 overrideDisableSingleAppOption,
-                hostUid,
-                mediaProjectionMetricsLogger
+                hostUid = 12345,
+                mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
             )
 
         dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index e46416c..ebab049 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -75,6 +75,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.statusbar.StatusBarState;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -146,6 +147,12 @@
         mTile.setTileSpec(SPEC);
     }
 
+    @After
+    public void destroyTile() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     @Test
     public void testClick_Metrics() {
         mTile.click(null /* expandable */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index 4c77fb8..27b6ea6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -27,6 +27,7 @@
 import com.android.internal.logging.MetricsLogger
 import com.android.settingslib.notification.data.repository.FakeZenModeRepository
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -41,10 +42,12 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
 import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SecureSettings
 import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -79,6 +82,10 @@
 
     @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider
 
+    @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
+
+    @Mock private lateinit var dialogDelegate: ModesDialogDelegate
+
     private val inputHandler = FakeQSTileIntentUserInputHandler()
     private val zenModeRepository = FakeZenModeRepository()
     private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository)
@@ -122,7 +129,13 @@
                 }
             )
 
-        userActionInteractor = ModesTileUserActionInteractor(inputHandler)
+        userActionInteractor =
+            ModesTileUserActionInteractor(
+                EmptyCoroutineContext,
+                inputHandler,
+                dialogTransitionAnimator,
+                dialogDelegate,
+            )
 
         underTest =
             ModesTile(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
index 3b0e194..bab9bbb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
@@ -16,13 +16,13 @@
 package com.android.systemui.screenshot.policy
 
 import android.content.ComponentName
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import android.graphics.Insets
 import android.graphics.Rect
 import android.os.UserHandle
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.systemui.screenshot.ImageCapture
 import com.android.systemui.screenshot.ScreenshotData
 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES
@@ -40,20 +40,23 @@
 @RunWith(AndroidJUnit4::class)
 class PolicyRequestProcessorTest {
 
-    val imageCapture = object : ImageCapture {
-        override fun captureDisplay(displayId: Int, crop: Rect?) = null
-        override suspend fun captureTask(taskId: Int) = null
-    }
+    val imageCapture =
+        object : ImageCapture {
+            override fun captureDisplay(displayId: Int, crop: Rect?) = null
+
+            override suspend fun captureTask(taskId: Int) = null
+        }
 
     /** Tests behavior when no policies are applied */
     @Test
     fun testProcess_defaultOwner_whenNoPolicyApplied() {
         val fullScreenWork = DisplayContentRepository {
-            singleFullScreen(TaskSpec(taskId = 1001, name = FILES, userId = WORK))
+            singleFullScreen(TaskSpec(taskId = TASK_ID, name = FILES, userId = WORK))
         }
 
         val request =
-            ScreenshotData(TAKE_SCREENSHOT_FULLSCREEN,
+            ScreenshotData(
+                TAKE_SCREENSHOT_FULLSCREEN,
                 SCREENSHOT_KEY_CHORD,
                 null,
                 topComponent = null,
@@ -61,24 +64,34 @@
                 taskId = -1,
                 insets = Insets.NONE,
                 bitmap = null,
-                displayId = DEFAULT_DISPLAY)
+                displayId = DEFAULT_DISPLAY
+            )
 
         /* Create a policy request processor with no capture policies */
         val requestProcessor =
-            PolicyRequestProcessor(Dispatchers.Unconfined,
+            PolicyRequestProcessor(
+                Dispatchers.Unconfined,
                 imageCapture,
                 policies = emptyList(),
                 defaultOwner = UserHandle.of(PERSONAL),
                 defaultComponent = ComponentName("default", "Component"),
-                displayTasks = fullScreenWork)
+                displayTasks = fullScreenWork
+            )
 
         val result = runBlocking { requestProcessor.process(request) }
 
-        assertWithMessage(
-            "With no policy, the screenshot should be assigned to the default user"
-        ).that(result.userHandle).isEqualTo(UserHandle.of(PERSONAL))
+        assertWithMessage("With no policy, the screenshot should be assigned to the default user")
+            .that(result.userHandle)
+            .isEqualTo(UserHandle.of(PERSONAL))
 
-        assertWithMessage("The topComponent of the screenshot").that(result.topComponent)
-                .isEqualTo(ComponentName.unflattenFromString(FILES))
+        assertWithMessage("The topComponent of the screenshot")
+            .that(result.topComponent)
+            .isEqualTo(ComponentName.unflattenFromString(FILES))
+
+        assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID)
+    }
+
+    companion object {
+        const val TASK_ID = 1001
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 169511f..967df39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -48,7 +48,12 @@
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.communal.util.CommunalColors
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
@@ -67,13 +72,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyFloat
-import org.mockito.Mock
-import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @ExperimentalCoroutinesApi
 @RunWith(AndroidTestingRunner::class)
@@ -87,11 +92,11 @@
             testDispatcher = UnconfinedTestDispatcher()
         }
 
-    @Mock private lateinit var communalViewModel: CommunalViewModel
-    @Mock private lateinit var powerManager: PowerManager
-    @Mock private lateinit var touchMonitor: TouchMonitor
-    @Mock private lateinit var communalColors: CommunalColors
-    @Mock private lateinit var communalContent: CommunalContent
+    private var communalViewModel = mock<CommunalViewModel>()
+    private var powerManager = mock<PowerManager>()
+    private var touchMonitor = mock<TouchMonitor>()
+    private var communalColors = mock<CommunalColors>()
+    private var communalContent = mock<CommunalContent>()
     private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
 
     private lateinit var parentView: FrameLayout
@@ -103,8 +108,6 @@
 
     @Before
     fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
         communalRepository = kosmos.fakeCommunalSceneRepository
 
         ambientTouchComponentFactory =
@@ -124,6 +127,7 @@
                     communalInteractor,
                     communalViewModel,
                     keyguardInteractor,
+                    kosmos.keyguardTransitionInteractor,
                     shadeInteractor,
                     powerManager,
                     communalColors,
@@ -167,6 +171,7 @@
                         communalInteractor,
                         communalViewModel,
                         keyguardInteractor,
+                        kosmos.keyguardTransitionInteractor,
                         shadeInteractor,
                         powerManager,
                         communalColors,
@@ -192,6 +197,7 @@
                     communalInteractor,
                     communalViewModel,
                     keyguardInteractor,
+                    kosmos.keyguardTransitionInteractor,
                     shadeInteractor,
                     powerManager,
                     communalColors,
@@ -212,6 +218,7 @@
                     communalInteractor,
                     communalViewModel,
                     keyguardInteractor,
+                    kosmos.keyguardTransitionInteractor,
                     shadeInteractor,
                     powerManager,
                     communalColors,
@@ -235,12 +242,15 @@
     }
 
     @Test
-    fun lifecycle_resumedAfterCommunalShows() {
-        // Communal is open.
-        goToScene(CommunalScenes.Communal)
+    fun lifecycle_resumedAfterCommunalShows() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
 
-        assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
-    }
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+            }
+        }
 
     @Test
     fun lifecycle_startedAfterCommunalCloses() =
@@ -289,6 +299,43 @@
         }
 
     @Test
+    fun lifecycle_startedWhenEditActivityShowing() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Edit activity is showing.
+                communalInteractor.setEditActivityShowing(true)
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
+    fun lifecycle_startedWhenEditModeTransitionStarted() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Leaving edit mode to return to the hub.
+                fakeKeyguardTransitionRepository.sendTransitionStep(
+                    TransitionStep(
+                        from = KeyguardState.GONE,
+                        to = KeyguardState.GLANCEABLE_HUB,
+                        value = 1.0f,
+                        transitionState = TransitionState.RUNNING
+                    )
+                )
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+            }
+        }
+
+    @Test
     fun lifecycle_createdAfterDisposeView() {
         // Container view disposed.
         underTest.disposeView()
@@ -312,6 +359,59 @@
         }
 
     @Test
+    fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Shade shows up.
+                shadeTestUtil.setShadeExpansion(1.0f)
+                testableLooper.processAllMessages()
+                underTest.onTouchEvent(DOWN_EVENT)
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+                // Shade starts collapsing.
+                shadeTestUtil.setShadeExpansion(.5f)
+                testableLooper.processAllMessages()
+                underTest.onTouchEvent(DOWN_EVENT)
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+                // Shade fully collpase, and then expand should with touch interaction should now
+                // be resumed.
+                shadeTestUtil.setShadeExpansion(0f)
+                testableLooper.processAllMessages()
+                shadeTestUtil.setShadeExpansion(.5f)
+                testableLooper.processAllMessages()
+                underTest.onTouchEvent(DOWN_EVENT)
+                testableLooper.processAllMessages()
+
+                assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+            }
+        }
+
+    @Test
+    fun touchHandling_moveEventProcessedAfterCancel() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Shade shows up.
+                shadeTestUtil.setQsExpansion(0.5f)
+                testableLooper.processAllMessages()
+                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+                assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue()
+                assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
+                assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
+            }
+        }
+
+    @Test
     fun editMode_communalAvailable() =
         with(kosmos) {
             testScope.runTest {
@@ -433,10 +533,10 @@
             testScope.runTest {
                 // Communal is closed.
                 goToScene(CommunalScenes.Blank)
-                `when`(
+                whenever(
                         notificationStackScrollLayoutController.isBelowLastNotification(
-                            anyFloat(),
-                            anyFloat()
+                            any(),
+                            any()
                         )
                     )
                     .thenReturn(false)
@@ -444,6 +544,62 @@
             }
         }
 
+    @Test
+    fun onTouchEvent_hubOpen_touchesDispatched() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Touch event is sent to the container view.
+                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+                verify(containerView).onTouchEvent(any())
+            }
+        }
+
+    @Test
+    fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Transitioning to or from edit mode.
+                communalInteractor.setEditActivityShowing(true)
+                testableLooper.processAllMessages()
+
+                // onTouchEvent returns true to consume the touch, but it is not sent to the
+                // container view.
+                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+                verify(containerView, never()).onTouchEvent(any())
+            }
+        }
+
+    @Test
+    fun onTouchEvent_editModeTransitionStarted_touchesConsumedButNotDispatched() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is open.
+                goToScene(CommunalScenes.Communal)
+
+                // Leaving edit mode to return to the hub.
+                fakeKeyguardTransitionRepository.sendTransitionStep(
+                    TransitionStep(
+                        from = KeyguardState.GONE,
+                        to = KeyguardState.GLANCEABLE_HUB,
+                        value = 1.0f,
+                        transitionState = TransitionState.RUNNING
+                    )
+                )
+                testableLooper.processAllMessages()
+
+                // onTouchEvent returns true to consume the touch, but it is not sent to the
+                // container view.
+                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+                verify(containerView, never()).onTouchEvent(any())
+            }
+        }
+
     private fun initAndAttachContainerView() {
         val mockInsets =
             mock<WindowInsets> {
@@ -462,8 +618,21 @@
         testableLooper.processAllMessages()
     }
 
-    private fun goToScene(scene: SceneKey) {
+    private suspend fun goToScene(scene: SceneKey) {
         communalRepository.changeScene(scene)
+        if (scene == CommunalScenes.Communal) {
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                kosmos.testScope
+            )
+        } else {
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.LOCKSCREEN,
+                kosmos.testScope
+            )
+        }
         testableLooper.processAllMessages()
     }
 
@@ -488,5 +657,35 @@
                 CONTAINER_HEIGHT.toFloat() / 2,
                 0
             )
+
+        private val CANCEL_EVENT =
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_CANCEL,
+                CONTAINER_WIDTH.toFloat() / 2,
+                CONTAINER_HEIGHT.toFloat() / 2,
+                0
+            )
+
+        private val MOVE_EVENT =
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_MOVE,
+                CONTAINER_WIDTH.toFloat() / 2,
+                CONTAINER_HEIGHT.toFloat() / 2,
+                0
+            )
+
+        private val UP_EVENT =
+            MotionEvent.obtain(
+                0L,
+                0L,
+                MotionEvent.ACTION_UP,
+                CONTAINER_WIDTH.toFloat() / 2,
+                CONTAINER_HEIGHT.toFloat() / 2,
+                0
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index b80d1a4..a5c4bcd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -18,7 +18,6 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -404,7 +403,6 @@
         mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
 
         mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR);
-        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
         mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
 
         mMainDispatcher = getMainDispatcher();
@@ -677,8 +675,14 @@
 
         mMainHandler = new Handler(Looper.getMainLooper());
 
+        LongPressHandlingView longPressHandlingView = mock(LongPressHandlingView.class);
         when(mView.requireViewById(R.id.keyguard_long_press))
-                .thenReturn(mock(LongPressHandlingView.class));
+                .thenReturn(longPressHandlingView);
+
+        Resources longPressHandlingViewRes = mock(Resources.class);
+        when(longPressHandlingView.getResources()).thenReturn(longPressHandlingViewRes);
+        when(longPressHandlingViewRes.getString(anyInt())).thenReturn("");
+
 
         mHeadsUpNotificationInteractor =
                 new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository,
@@ -801,7 +805,6 @@
                 .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
         verify(mNotificationStackScrollLayoutController)
                 .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
-        verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true);
         reset(mKeyguardStatusViewController);
 
         when(mNotificationPanelViewControllerLazy.get())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 90e8ea5f..905cc4c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -92,6 +92,7 @@
      * When the Back gesture starts (progress 0%), the scrim will stay at 100% scale (1.0f).
      */
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testBackGesture_min_scrimAtMaxScale() {
         mNotificationPanelViewController.onBackProgressed(0.0f);
         verify(mScrimController).applyBackScaling(1.0f);
@@ -101,6 +102,7 @@
      * When the Back gesture is at max (progress 100%), the scrim will be scaled to its minimum.
      */
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testBackGesture_max_scrimAtMinScale() {
         mNotificationPanelViewController.onBackProgressed(1.0f);
         verify(mScrimController).applyBackScaling(
@@ -108,6 +110,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onNotificationHeightChangeWhileOnKeyguardWillComputeMaxKeyguardNotifications() {
         mStatusBarStateController.setState(KEYGUARD);
         ArgumentCaptor<OnHeightChangedListener> captor =
@@ -124,6 +127,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onNotificationHeightChangeWhileInShadeWillNotComputeMaxKeyguardNotifications() {
         mStatusBarStateController.setState(SHADE);
         ArgumentCaptor<OnHeightChangedListener> captor =
@@ -140,6 +144,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void computeMaxKeyguardNotifications_lockscreenToShade_returnsExistingMax() {
         when(mAmbientState.getFractionToShade()).thenReturn(0.5f);
         mNotificationPanelViewController.setMaxDisplayedNotifications(-1);
@@ -150,6 +155,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void computeMaxKeyguardNotifications_noTransition_updatesMax() {
         when(mAmbientState.getFractionToShade()).thenReturn(0f);
         mNotificationPanelViewController.setMaxDisplayedNotifications(-1);
@@ -196,6 +202,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getVerticalSpaceForLockscreenShelf_useLockIconBottomPadding_returnsShelfHeight() {
         enableSplitShade(/* enabled= */ false);
         setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -213,6 +220,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getVerticalSpaceForLockscreenShelf_useIndicationBottomPadding_returnsZero() {
         enableSplitShade(/* enabled= */ false);
         setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -230,6 +238,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getVerticalSpaceForLockscreenShelf_useAmbientBottomPadding_returnsZero() {
         enableSplitShade(/* enabled= */ false);
         setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -247,6 +256,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getVerticalSpaceForLockscreenShelf_useLockIconPadding_returnsLessThanShelfHeight() {
         enableSplitShade(/* enabled= */ false);
         setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -264,6 +274,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getVerticalSpaceForLockscreenShelf_splitShade() {
         enableSplitShade(/* enabled= */ true);
         setBottomPadding(/* stackScrollLayoutBottom= */ 100,
@@ -281,6 +292,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSetPanelScrimMinFractionWhenHeadsUpIsDragged() {
         mNotificationPanelViewController.setHeadsUpDraggingStartingHeight(
                 mNotificationPanelViewController.getMaxPanelHeight() / 2);
@@ -288,6 +300,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSetDozing_notifiesNsslAndStateController() {
         mNotificationPanelViewController.setDozing(true /* dozing */, false /* animate */);
         verify(mNotificationStackScrollLayoutController).setDozing(eq(true), eq(false));
@@ -295,6 +308,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testOnDozeAmountChanged_positionClockAndNotificationsUsesUdfpsLocation() {
         // GIVEN UDFPS is enrolled and we're on the keyguard
         final Point udfpsLocationCenter = new Point(0, 100);
@@ -332,12 +346,14 @@
 
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSetExpandedHeight() {
         mNotificationPanelViewController.setExpandedHeight(200);
         assertThat((int) mNotificationPanelViewController.getExpandedHeight()).isEqualTo(200);
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testOnTouchEvent_expansionCanBeBlocked() {
         onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0));
         onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 200f, 0));
@@ -350,6 +366,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void test_pulsing_onTouchEvent_noTracking() {
         // GIVEN device is pulsing
         mNotificationPanelViewController.setPulsing(true);
@@ -367,6 +384,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void alternateBouncerVisible_onTouchEvent_notHandled() {
         mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
         // GIVEN alternate bouncer is visible
@@ -385,6 +403,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void test_onTouchEvent_startTracking() {
         // GIVEN device is NOT pulsing
         mNotificationPanelViewController.setPulsing(false);
@@ -402,9 +421,8 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onInterceptTouchEvent_nsslMigrationOff_userActivity() {
-        mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */,
                 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
                 0 /* metaState */));
@@ -413,9 +431,8 @@
     }
 
     @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onInterceptTouchEvent_nsslMigrationOn_userActivity_not_called() {
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
         mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */,
                 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
                 0 /* metaState */));
@@ -424,6 +441,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testOnTouchEvent_expansionResumesAfterBriefTouch() {
         mFalsingManager.setIsClassifierEnabled(true);
         mFalsingManager.setIsFalseTouch(false);
@@ -460,6 +478,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testA11y_initializeNode() {
         AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
         mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo);
@@ -473,6 +492,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testA11y_scrollForward() {
         mAccessibilityDelegate.performAccessibilityAction(
                 mView,
@@ -483,6 +503,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testA11y_scrollUp() {
         mAccessibilityDelegate.performAccessibilityAction(
                 mView,
@@ -493,6 +514,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testKeyguardStatusViewInSplitShade_changesConstraintsDependingOnNotifications() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(/* enabled= */ true);
@@ -511,6 +533,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_splitShade_dozing_alwaysDozingOn_isCentered() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -523,6 +546,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_splitShade_dozing_alwaysDozingOff_isNotCentered() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -535,6 +559,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_splitShade_notDozing_alwaysDozingOn_isNotCentered() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -547,6 +572,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_splitShade_pulsing_isNotCentered() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -560,6 +586,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_splitShade_notPulsing_isNotCentered() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -573,6 +600,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_singleShade_isCentered() {
         enableSplitShade(/* enabled= */ false);
         // The conditions below would make the clock NOT be centered on split shade.
@@ -587,6 +615,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -602,6 +631,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_willPlayDelayedDoze_notifiesKeyguardMediaController() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true);
@@ -614,6 +644,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() {
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
         when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false);
@@ -629,6 +660,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() {
         ArgumentCaptor<View.OnLayoutChangeListener> captor =
                 ArgumentCaptor.forClass(View.OnLayoutChangeListener.class);
@@ -646,6 +678,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -653,6 +686,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testCanCollapsePanelOnTouch_trueWhenScrolledToBottom() {
         mStatusBarStateController.setState(SHADE);
         when(mNotificationStackScrollLayoutController.isScrolledToBottom()).thenReturn(true);
@@ -661,6 +695,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testCanCollapsePanelOnTouch_trueWhenInSettings() {
         mStatusBarStateController.setState(SHADE);
         when(mQsController.getExpanded()).thenReturn(true);
@@ -669,6 +704,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testCanCollapsePanelOnTouch_falseInDualPaneShade() {
         mStatusBarStateController.setState(SHADE);
         enableSplitShade(/* enabled= */ true);
@@ -695,6 +731,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testCancelSwipeWhileLocked_notifiesKeyguardState() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -707,6 +744,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSwipe_exactlyToTarget_notifiesNssl() {
         // No over-expansion
         mNotificationPanelViewController.setOverExpansion(0f);
@@ -722,6 +760,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testRotatingToSplitShadeWithQsExpanded_transitionsToShadeLocked() {
         mStatusBarStateController.setState(KEYGUARD);
         when(mQsController.getExpanded()).thenReturn(true);
@@ -732,6 +771,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() {
         enableSplitShade(true);
         mStatusBarStateController.setState(SHADE);
@@ -741,6 +781,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testLockedSplitShadeTransitioningToKeyguard_closesQS() {
         enableSplitShade(true);
         mStatusBarStateController.setState(SHADE_LOCKED);
@@ -750,6 +791,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSwitchesToCorrectClockInSinglePaneShade() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -765,6 +807,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSwitchesToCorrectClockInSplitShade() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(/* enabled= */ true);
@@ -785,6 +828,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testHasNotifications_switchesToLargeClockWhenEnteringSplitShade() {
         mStatusBarStateController.setState(KEYGUARD);
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1);
@@ -796,6 +840,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testNoNotifications_switchesToLargeClockWhenEnteringSplitShade() {
         mStatusBarStateController.setState(KEYGUARD);
         when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0);
@@ -807,6 +852,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testHasNotifications_switchesToSmallClockWhenExitingSplitShade() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(/* enabled= */ true);
@@ -820,6 +866,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testNoNotifications_switchesToLargeClockWhenExitingSplitShade() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(/* enabled= */ true);
@@ -833,6 +880,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void clockSize_mediaShowing_inSplitShade_onAod_isLarge() {
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         mStatusBarStateController.setState(KEYGUARD);
@@ -848,6 +896,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void clockSize_mediaShowing_inSplitShade_screenOff_notAod_isSmall() {
         when(mDozeParameters.getAlwaysOn()).thenReturn(false);
         mStatusBarStateController.setState(KEYGUARD);
@@ -863,6 +912,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_showNSSL() {
         // GIVEN
         mStatusBarStateController.setState(KEYGUARD);
@@ -883,6 +933,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_hideNSSL() {
         // GIVEN
         mStatusBarStateController.setState(KEYGUARD);
@@ -904,6 +955,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() {
         when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
         mStatusBarStateController.setState(KEYGUARD);
@@ -919,6 +971,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void switchesToBigClockInSplitShadeOn_landFlagOn_ForceSmallClock() {
         when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
         mStatusBarStateController.setState(KEYGUARD);
@@ -938,6 +991,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void switchesToBigClockInSplitShadeOn_landFlagOff_DontForceSmallClock() {
         when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false);
         mStatusBarStateController.setState(KEYGUARD);
@@ -957,6 +1011,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(/* enabled= */ true);
@@ -978,6 +1033,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testFoldToAodAnimationCleansupInAnimationEnd() {
         ArgumentCaptor<Animator.AnimatorListener> animCaptor =
                 ArgumentCaptor.forClass(Animator.AnimatorListener.class);
@@ -997,6 +1053,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testExpandWithQsMethodIsUsingLockscreenTransitionController() {
         enableSplitShade(/* enabled= */ true);
         mStatusBarStateController.setState(KEYGUARD);
@@ -1008,6 +1065,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void setKeyguardStatusBarAlpha_setsAlphaOnKeyguardStatusBarController() {
         float statusBarAlpha = 0.5f;
 
@@ -1017,6 +1075,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
         enableSplitShade(/* enabled= */ true);
         mShadeExpansionStateManager.updateState(STATE_OPEN);
@@ -1030,6 +1089,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
         enableSplitShade(/* enabled= */ true);
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
@@ -1042,6 +1102,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testQsImmediateResetsWhenPanelOpensOrCloses() {
         mShadeExpansionStateManager.updateState(STATE_OPEN);
         mShadeExpansionStateManager.updateState(STATE_CLOSED);
@@ -1049,6 +1110,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
         when(mCommandQueue.panelsEnabled()).thenReturn(true);
 
@@ -1065,6 +1127,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testPanelClosedWhenClosingQsInSplitShade() {
         mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
                 /* expanded= */ true, /* tracking= */ false);
@@ -1078,6 +1141,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getMaxPanelTransitionDistance_expanding_inSplitShade_returnsSplitShadeFullTransitionDistance() {
         enableSplitShade(true);
         mNotificationPanelViewController.expandToQs();
@@ -1088,6 +1152,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void isExpandingOrCollapsing_returnsTrue_whenQsLockscreenDragInProgress() {
         when(mQsController.getLockscreenShadeDragProgress()).thenReturn(0.5f);
         assertThat(mNotificationPanelViewController.isExpandingOrCollapsing()).isTrue();
@@ -1095,6 +1160,7 @@
 
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getMaxPanelTransitionDistance_inSplitShade_withHeadsUp_returnsBiggerValue() {
         enableSplitShade(true);
         mNotificationPanelViewController.expandToQs();
@@ -1111,6 +1177,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getMaxPanelTransitionDistance_expandingSplitShade_keyguard_returnsNonSplitShadeValue() {
         mStatusBarStateController.setState(KEYGUARD);
         enableSplitShade(true);
@@ -1122,6 +1189,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getMaxPanelTransitionDistance_expanding_notSplitShade_returnsNonSplitShadeValue() {
         enableSplitShade(false);
         mNotificationPanelViewController.expandToQs();
@@ -1132,6 +1200,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() {
         setIsFullWidth(true);
 
@@ -1139,6 +1208,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() {
         setIsFullWidth(false);
 
@@ -1146,6 +1216,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onLayoutChange_qsNotSet_doesNotCrash() {
         mQuickSettingsController.setQs(null);
 
@@ -1153,6 +1224,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
         StatusBarStateController.StateListener statusBarStateListener =
                 mNotificationPanelViewController.getStatusBarStateListener();
@@ -1167,8 +1239,8 @@
     }
 
     @Test
+    @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void nsslFlagEnabled_allowOnlyExternalTouches() {
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
 
         // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
         mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
@@ -1179,6 +1251,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onSplitShadeChanged_duringShadeExpansion_resetsOverScrollState() {
         // There was a bug where there was left-over overscroll state after going from split shade
         // to single shade.
@@ -1200,6 +1273,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onSplitShadeChanged_alwaysResetsOverScrollState() {
         enableSplitShade(true);
         enableSplitShade(false);
@@ -1217,6 +1291,7 @@
      * to ensure scrollY can be correctly set to be 0
      */
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
         // Given: Shade is expanded
         mNotificationPanelViewController.notifyExpandingFinished();
@@ -1237,6 +1312,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void onShadeFlingEnd_mExpandImmediateShouldBeReset() {
         mNotificationPanelViewController.onFlingEnd(false);
 
@@ -1244,6 +1320,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() {
         mStatusBarStateController.setState(SHADE);
         enableSplitShade(true);
@@ -1253,6 +1330,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeFullyExpanded_inShadeState() {
         mStatusBarStateController.setState(SHADE);
 
@@ -1265,6 +1343,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeFullyExpanded_onKeyguard() {
         mStatusBarStateController.setState(KEYGUARD);
 
@@ -1274,12 +1353,14 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeFullyExpanded_onShadeLocked() {
         mStatusBarStateController.setState(SHADE_LOCKED);
         assertThat(mNotificationPanelViewController.isShadeFullyExpanded()).isTrue();
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeExpanded_whenHasHeight() {
         int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance();
         mNotificationPanelViewController.setExpandedHeight(transitionDistance);
@@ -1287,6 +1368,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeExpanded_whenInstantExpanding() {
         mNotificationPanelViewController.expand(true);
         assertThat(mNotificationPanelViewController.isExpanded()).isTrue();
@@ -1300,12 +1382,14 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeExpanded_whenUnlockedOffscreenAnimationRunning() {
         when(mUnlockedScreenOffAnimationController.isAnimationPlaying()).thenReturn(true);
         assertThat(mNotificationPanelViewController.isExpanded()).isTrue();
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeExpanded_whenInputFocusTransferStarted() {
         when(mCommandQueue.panelsEnabled()).thenReturn(true);
 
@@ -1315,6 +1399,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void shadeNotExpanded_whenInputFocusTransferStartedButPanelsDisabled() {
         when(mCommandQueue.panelsEnabled()).thenReturn(false);
 
@@ -1324,6 +1409,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void cancelInputFocusTransfer_shadeCollapsed() {
         when(mCommandQueue.panelsEnabled()).thenReturn(true);
         mNotificationPanelViewController.startInputFocusTransfer();
@@ -1334,6 +1420,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void finishInputFocusTransfer_shadeFlingingOpen() {
         when(mCommandQueue.panelsEnabled()).thenReturn(true);
         mNotificationPanelViewController.startInputFocusTransfer();
@@ -1344,6 +1431,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getFalsingThreshold_deviceNotInteractive_isQsThreshold() {
         PowerInteractor.Companion.setAsleepForTest(
                 mPowerInteractor, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
@@ -1353,6 +1441,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getFalsingThreshold_lastWakeNotDueToTouch_isQsThreshold() {
         PowerInteractor.Companion.setAwakeForTest(
                 mPowerInteractor, PowerManager.WAKE_REASON_POWER_BUTTON);
@@ -1362,6 +1451,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void getFalsingThreshold_lastWakeDueToTouch_greaterThanQsThreshold() {
         PowerInteractor.Companion.setAwakeForTest(mPowerInteractor, PowerManager.WAKE_REASON_TAP);
         when(mQsController.getFalsingThreshold()).thenReturn(14);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index e1d92e7..64eadb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.shade
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.testing.TestableLooper
 import android.view.HapticFeedbackConstants
@@ -27,6 +28,7 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.util.CollectionUtils
 import com.android.keyguard.KeyguardClockSwitch.LARGE
+import com.android.systemui.Flags
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
@@ -34,7 +36,6 @@
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -58,6 +59,7 @@
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
+@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
 class NotificationPanelViewControllerWithCoroutinesTest :
     NotificationPanelViewControllerBaseTest() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 74a2999..6f2302a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.shade
 
 import android.content.Context
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.flag.junit.FlagsParameterization
 import android.testing.TestableLooper
@@ -31,6 +33,7 @@
 import com.android.keyguard.LegacyLockIconViewController
 import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
@@ -398,8 +401,8 @@
 
     @Test
     @DisableSceneContainer
+    @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun handleDispatchTouchEvent_nsslMigrationOff_userActivity_not_called() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
         underTest.setStatusBarViewController(phoneStatusBarViewController)
 
         interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
@@ -408,8 +411,8 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun handleDispatchTouchEvent_nsslMigrationOn_userActivity() {
-        enableMigrateClocksFlag()
         underTest.setStatusBarViewController(phoneStatusBarViewController)
 
         interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
@@ -440,6 +443,7 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun shouldInterceptTouchEvent_dozing_touchInLockIconArea_touchNotIntercepted() {
         // GIVEN dozing
         whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -452,13 +456,12 @@
         // AND the lock icon wants the touch
         whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(true)
 
-        enableMigrateClocksFlag()
-
         // THEN touch should NOT be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isFalse()
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun shouldInterceptTouchEvent_dozing_touchNotInLockIconArea_touchIntercepted() {
         // GIVEN dozing
         whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -471,13 +474,12 @@
         whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any()))
                 .thenReturn(false)
 
-        enableMigrateClocksFlag()
-
         // THEN touch should be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun shouldInterceptTouchEvent_dozing_touchInStatusBar_touchIntercepted() {
         // GIVEN dozing
         whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -490,13 +492,12 @@
         whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any()))
                 .thenReturn(true)
 
-        enableMigrateClocksFlag()
-
         // THEN touch should be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun shouldInterceptTouchEvent_dozingAndPulsing_touchIntercepted() {
         // GIVEN dozing
         whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -517,8 +518,6 @@
         whenever(shadeViewController.handleExternalInterceptTouch(DOWN_EVENT))
                 .thenReturn(true)
 
-        enableMigrateClocksFlag()
-
         // THEN touch should be intercepted by NotificationShade
         assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue()
     }
@@ -652,19 +651,13 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun cancelCurrentTouch_callsDragDownHelper() {
-        enableMigrateClocksFlag()
         underTest.cancelCurrentTouch()
 
         verify(dragDownHelper).stopDragging()
     }
 
-    private fun enableMigrateClocksFlag() {
-        if (!Flags.migrateClocksToBlueprint()) {
-            mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-        }
-    }
-
     companion object {
         private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
         private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index fec7424..ca29dd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.shade
 
 import android.os.SystemClock
+import android.platform.test.annotations.DisableFlags
 import android.testing.TestableLooper.RunWithLooper
 import android.view.MotionEvent
 import android.widget.FrameLayout
@@ -208,9 +209,9 @@
     }
 
     @Test
+    @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun testDragDownHelperCalledWhenDraggingDown() =
         testScope.runTest {
-            mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
             whenever(dragDownHelper.isDraggingDown).thenReturn(true)
             val now = SystemClock.elapsedRealtime()
             val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index c9a7c82..02764f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
+import android.content.DialogInterface
 import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
@@ -37,6 +41,8 @@
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.policy.CastDevice
@@ -45,7 +51,10 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -60,6 +69,16 @@
 
     private val mockScreenCastDialog = mock<SystemUIDialog>()
     private val mockGenericCastDialog = mock<SystemUIDialog>()
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
 
     private val underTest = kosmos.castToOtherDeviceChipViewModel
 
@@ -193,6 +212,63 @@
         }
 
     @Test
+    fun chip_projectionStoppedFromDialog_chipImmediatelyHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+            // WHEN the stop action on the dialog is clicked
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockScreenCastDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden...
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            // ...even though the repo still says it's projecting
+            assertThat(mediaProjectionRepo.mediaProjectionState.value)
+                .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+            // AND we specify no animation
+            assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+        }
+
+    @Test
+    fun chip_routeStoppedFromDialog_chipImmediatelyHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+            // WHEN the stop action on the dialog is clicked
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockGenericCastDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden...
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            // ...even though the repo still says it's projecting
+            assertThat(mediaRouterRepo.castDevices.value).isNotEmpty()
+
+            // AND we specify no animation
+            assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+        }
+
+    @Test
     fun chip_colorsAreRed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -297,8 +373,14 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
-            verify(mockScreenCastDialog).show()
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockScreenCastDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
         }
 
     @Test
@@ -316,8 +398,14 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
-            verify(mockScreenCastDialog).show()
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockScreenCastDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
         }
 
     @Test
@@ -339,7 +427,70 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
-            verify(mockGenericCastDialog).show()
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockGenericCastDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
+        }
+
+    @Test
+    fun chip_projectionStateCasting_clickListenerHasCuj() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            clickListener!!.onClick(chipView)
+
+            val cujCaptor = argumentCaptor<DialogCuj>()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    any(),
+                    any(),
+                    cujCaptor.capture(),
+                    anyBoolean(),
+                )
+
+            assertThat(cujCaptor.firstValue.cujType)
+                .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+            assertThat(cujCaptor.firstValue.tag).contains("Cast")
+        }
+
+    @Test
+    fun chip_routerStateCasting_clickListenerHasCuj() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaRouterRepo.castDevices.value =
+                listOf(
+                    CastDevice(
+                        state = CastDevice.CastState.Connected,
+                        id = "id",
+                        name = "name",
+                        description = "desc",
+                        origin = CastDevice.CastOrigin.MediaRouter,
+                    )
+                )
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            clickListener!!.onClick(chipView)
+
+            val cujCaptor = argumentCaptor<DialogCuj>()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    any(),
+                    any(),
+                    cujCaptor.capture(),
+                    anyBoolean(),
+                )
+
+            assertThat(cujCaptor.firstValue.cujType)
+                .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+            assertThat(cujCaptor.firstValue.tag).contains("Cast")
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index 4728c64..b4a37ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
 
+import android.content.DialogInterface
 import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+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
@@ -30,9 +34,13 @@
 import com.android.systemui.res.R
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
@@ -40,7 +48,10 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -53,11 +64,22 @@
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
     private val systemClock = kosmos.fakeSystemClock
     private val mockSystemUIDialog = mock<SystemUIDialog>()
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
 
     private val underTest = kosmos.screenRecordChipViewModel
 
     @Before
     fun setUp() {
+        setUpPackageManagerForMediaProjection(kosmos)
         whenever(kosmos.mockSystemUIDialogFactory.create(any<EndScreenRecordingDialogDelegate>()))
             .thenReturn(mockSystemUIDialog)
     }
@@ -132,6 +154,40 @@
         }
 
     @Test
+    fun chip_recordingStoppedFromDialog_screenRecordAndShareToAppChipImmediatelyHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            val latestShareToApp by collectLastValue(kosmos.shareToAppChipViewModel.chip)
+
+            // On real devices, when screen recording is active then share-to-app is also active
+            // because screen record is just a special case of share-to-app where the app receiving
+            // the share is SysUI
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen("fake.package")
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+            // WHEN the stop action on the dialog is clicked
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN both the screen record chip and the share-to-app chip are immediately hidden...
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            // ...even though the repos still say it's recording
+            assertThat(screenRecordRepo.screenRecordState.value)
+                .isEqualTo(ScreenRecordModel.Recording)
+            assertThat(mediaProjectionRepo.mediaProjectionState.value)
+                .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+            // AND we specify no animation
+            assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+        }
+
+    @Test
     fun chip_startingState_colorsAreRed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -182,9 +238,15 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(mockSystemUIDialog).show()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockSystemUIDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
         }
 
     @Test
@@ -198,9 +260,15 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(mockSystemUIDialog).show()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockSystemUIDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
         }
 
     @Test
@@ -218,8 +286,39 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
-            verify(mockSystemUIDialog).show()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockSystemUIDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
+        }
+
+    @Test
+    fun chip_clickListenerHasCuj() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen("host.package")
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            clickListener!!.onClick(chipView)
+
+            val cujCaptor = argumentCaptor<DialogCuj>()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    any(),
+                    any(),
+                    cujCaptor.capture(),
+                    anyBoolean(),
+                )
+
+            assertThat(cujCaptor.firstValue.cujType)
+                .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+            assertThat(cujCaptor.firstValue.tag).contains("Screen record")
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index f87b17d..2658679 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
+import android.content.DialogInterface
 import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -34,6 +38,8 @@
 import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
@@ -41,7 +47,10 @@
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -54,6 +63,16 @@
     private val systemClock = kosmos.fakeSystemClock
 
     private val mockShareDialog = mock<SystemUIDialog>()
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
 
     private val underTest = kosmos.shareToAppChipViewModel
 
@@ -134,6 +153,31 @@
         }
 
     @Test
+    fun chip_shareStoppedFromDialog_chipImmediatelyHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+
+            // WHEN the stop action on the dialog is clicked
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden...
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            // ...even though the repo still says it's projecting
+            assertThat(mediaProjectionRepo.mediaProjectionState.value)
+                .isInstanceOf(MediaProjectionState.Projecting::class.java)
+
+            // AND we specify no animation
+            assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse()
+        }
+
+    @Test
     fun chip_colorsAreRed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
@@ -181,8 +225,14 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
-            verify(mockShareDialog).show()
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockShareDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
         }
 
     @Test
@@ -199,7 +249,41 @@
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
             assertThat(clickListener).isNotNull()
 
-            clickListener!!.onClick(mock<View>())
-            verify(mockShareDialog).show()
+            clickListener!!.onClick(chipView)
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    eq(mockShareDialog),
+                    eq(chipBackgroundView),
+                    any(),
+                    anyBoolean(),
+                )
+        }
+
+    @Test
+    fun chip_clickListenerHasCuj() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    hostDeviceName = null,
+                    createTask(taskId = 1),
+                )
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            clickListener!!.onClick(chipView)
+
+            val cujCaptor = argumentCaptor<DialogCuj>()
+            verify(kosmos.mockDialogTransitionAnimator)
+                .showFromView(
+                    any(),
+                    any(),
+                    cujCaptor.capture(),
+                    anyBoolean(),
+                )
+
+            assertThat(cujCaptor.firstValue.cujType)
+                .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
+            assertThat(cujCaptor.firstValue.tag).contains("Share")
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
new file mode 100644
index 0000000..b9049e8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+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.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.model.ColorsModel
+import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class ChipTransitionHelperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+
+    @Test
+    fun createChipFlow_typicallyFollowsInputFlow() =
+        testScope.runTest {
+            val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+            val inputChipFlow =
+                MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+            val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+            val newChip =
+                OngoingActivityChipModel.Shown.Timer(
+                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    colors = ColorsModel.Themed,
+                    startTimeMs = 100L,
+                    onClickListener = null,
+                )
+
+            inputChipFlow.value = newChip
+
+            assertThat(latest).isEqualTo(newChip)
+
+            val newerChip =
+                OngoingActivityChipModel.Shown.IconOnly(
+                    icon = Icon.Resource(R.drawable.ic_hotspot, contentDescription = null),
+                    colors = ColorsModel.Themed,
+                    onClickListener = null,
+                )
+
+            inputChipFlow.value = newerChip
+
+            assertThat(latest).isEqualTo(newerChip)
+        }
+
+    @Test
+    fun activityStopped_chipHiddenWithoutAnimationFor500ms() =
+        testScope.runTest {
+            val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+            val inputChipFlow =
+                MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+            val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+            val shownChip =
+                OngoingActivityChipModel.Shown.Timer(
+                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    colors = ColorsModel.Themed,
+                    startTimeMs = 100L,
+                    onClickListener = null,
+                )
+
+            inputChipFlow.value = shownChip
+
+            assertThat(latest).isEqualTo(shownChip)
+
+            // WHEN #onActivityStopped is invoked
+            underTest.onActivityStoppedFromDialog()
+            runCurrent()
+
+            // THEN the chip is hidden and has no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+            // WHEN only 250ms have elapsed
+            advanceTimeBy(250)
+
+            // THEN the chip is still hidden
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+            // WHEN over 500ms have elapsed
+            advanceTimeBy(251)
+
+            // THEN the chip returns to the original input flow value
+            assertThat(latest).isEqualTo(shownChip)
+        }
+
+    @Test
+    fun activityStopped_stoppedAgainBefore500ms_chipReshownAfterSecond500ms() =
+        testScope.runTest {
+            val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope)
+            val inputChipFlow =
+                MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
+            val latest by collectLastValue(underTest.createChipFlow(inputChipFlow))
+
+            val shownChip =
+                OngoingActivityChipModel.Shown.Timer(
+                    icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null),
+                    colors = ColorsModel.Themed,
+                    startTimeMs = 100L,
+                    onClickListener = null,
+                )
+
+            inputChipFlow.value = shownChip
+
+            assertThat(latest).isEqualTo(shownChip)
+
+            // WHEN #onActivityStopped is invoked
+            underTest.onActivityStoppedFromDialog()
+            runCurrent()
+
+            // THEN the chip is hidden and has no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+
+            // WHEN 250ms have elapsed, get another stop event
+            advanceTimeBy(250)
+            underTest.onActivityStoppedFromDialog()
+            runCurrent()
+
+            // THEN the chip is still hidden for another 500ms afterwards
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+            advanceTimeBy(499)
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+            advanceTimeBy(2)
+            assertThat(latest).isEqualTo(shownChip)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
index ca043f1..6e4d886 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
@@ -18,32 +18,58 @@
 
 import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import kotlin.test.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 class OngoingActivityChipViewModelTest : SysuiTestCase() {
     private val mockSystemUIDialog = mock<SystemUIDialog>()
     private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog }
+    private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
 
     @Test
     fun createDialogLaunchOnClickListener_showsDialogOnClick() {
+        val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test")
         val clickListener =
             createDialogLaunchOnClickListener(
                 dialogDelegate,
+                dialogTransitionAnimator,
+                cuj,
                 logcatLogBuffer("OngoingActivityChipViewModelTest"),
                 "tag",
             )
 
-        // Dialogs must be created on the main thread
-        context.mainExecutor.execute {
-            clickListener.onClick(mock<View>())
-            verify(mockSystemUIDialog).show()
-        }
+        clickListener.onClick(chipView)
+        verify(dialogTransitionAnimator)
+            .showFromView(
+                eq(mockSystemUIDialog),
+                eq(chipBackgroundView),
+                eq(cuj),
+                anyBoolean(),
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index b1a8d0b..ee249f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -16,6 +16,10 @@
 
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
+import android.content.DialogInterface
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -33,10 +37,14 @@
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runCurrent
@@ -44,9 +52,17 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
@@ -56,6 +72,18 @@
     private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
     private val callRepo = kosmos.ongoingCallRepository
 
+    private val mockSystemUIDialog = mock<SystemUIDialog>()
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
+
     private val underTest = kosmos.ongoingActivityChipsViewModel
 
     @Before
@@ -72,7 +100,7 @@
 
             val latest by collectLastValue(underTest.chip)
 
-            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
         }
 
     @Test
@@ -230,7 +258,81 @@
             job2.cancel()
         }
 
+    @Test
+    fun chip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() =
+        testScope.runTest {
+            screenRecordState.value = ScreenRecordModel.Recording
+            mediaProjectionState.value = MediaProjectionState.NotProjecting
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.chip)
+
+            assertIsScreenRecordChip(latest)
+
+            // WHEN screen record gets stopped via dialog
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden with no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+        }
+
+    @Test
+    fun chip_projectionStoppedViaDialog_chipHiddenWithoutAnimation() =
+        testScope.runTest {
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+            screenRecordState.value = ScreenRecordModel.DoingNothing
+            callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+
+            val latest by collectLastValue(underTest.chip)
+
+            assertIsShareToAppChip(latest)
+
+            // WHEN media projection gets stopped via dialog
+            val dialogStopAction =
+                getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos)
+            dialogStopAction.onClick(mock<DialogInterface>(), 0)
+
+            // THEN the chip is immediately hidden with no animation
+            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
+        }
+
     companion object {
+        /**
+         * Assuming that the click listener in [latest] opens a dialog, this fetches the action
+         * associated with the positive button, which we assume is the "Stop sharing" action.
+         */
+        fun getStopActionFromDialog(
+            latest: OngoingActivityChipModel?,
+            chipView: View,
+            dialog: SystemUIDialog,
+            kosmos: Kosmos
+        ): DialogInterface.OnClickListener {
+            // Capture the action that would get invoked when the user clicks "Stop" on the dialog
+            lateinit var dialogStopAction: DialogInterface.OnClickListener
+            Mockito.doAnswer {
+                    val delegate = it.arguments[0] as SystemUIDialog.Delegate
+                    delegate.beforeCreate(dialog, /* savedInstanceState= */ null)
+
+                    val stopActionCaptor = argumentCaptor<DialogInterface.OnClickListener>()
+                    verify(dialog).setPositiveButton(any(), stopActionCaptor.capture())
+                    dialogStopAction = stopActionCaptor.firstValue
+
+                    return@doAnswer dialog
+                }
+                .whenever(kosmos.mockSystemUIDialogFactory)
+                .create(any<SystemUIDialog.Delegate>())
+            whenever(kosmos.packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
+                .thenThrow(PackageManager.NameNotFoundException())
+            // Click the chip so that we open the dialog and we fill in [dialogStopAction]
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            clickListener!!.onClick(chipView)
+
+            return dialogStopAction
+        }
+
         fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index acb005f..0407fc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -42,7 +42,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 // this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     lateinit var manager: NotificationSectionsFeatureManager
     private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index d87b3e2..4218be2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -13,88 +13,57 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@file:OptIn(ExperimentalCoroutinesApi::class)
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.app.Notification
-import android.os.UserHandle
-import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.andSceneContainer
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.kosmos.Kosmos
-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.setTransition
-import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.policy.HeadsUpManager
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth.assertThat
 import java.util.function.Consumer
-import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScheduler
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.same
-import org.mockito.Mockito.anyString
 import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() {
+@RunWith(AndroidJUnit4::class)
+class KeyguardCoordinatorTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos()
-
-    private val headsUpManager: HeadsUpManager = mock()
     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
-    private val keyguardRepository = FakeKeyguardRepository()
-    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val notifPipeline: NotifPipeline = mock()
     private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
     private val statusBarStateController: StatusBarStateController = mock()
 
-    init {
-        mSetFlagsRule.setFlagsParameterization(flags)
+    private lateinit var onStateChangeListener: Consumer<String>
+
+    @Before
+    fun setup() {
+        val keyguardCoordinator =
+            KeyguardCoordinator(
+                keyguardNotifVisibilityProvider,
+                sectionHeaderVisibilityProvider,
+                statusBarStateController,
+            )
+        keyguardCoordinator.attach(notifPipeline)
+        onStateChangeListener =
+            argumentCaptor {
+                    verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+                }
+                .lastValue
     }
 
     @Test
-    fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest {
+    fun testSetSectionHeadersVisibleInShade() {
         clearInvocations(sectionHeaderVisibilityProvider)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
         onStateChangeListener.accept("state change")
@@ -102,617 +71,10 @@
     }
 
     @Test
-    fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest {
+    fun testSetSectionHeadersNotVisibleOnKeyguard() {
         clearInvocations(sectionHeaderVisibilityProvider)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         onStateChangeListener.accept("state change")
         verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
     }
-
-    @Test
-    fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
-        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is recognized as "seen" and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
-            // WHEN: The keyguard goes away
-            keyguardRepository.setKeyguardShowing(false)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is shown regardless
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() {
-        // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(false)
-        runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: The device transitions to AOD
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.AOD,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // THEN: We are no longer listening for shade expansions
-            verify(statusBarStateController, never()).addCallback(any())
-        }
-    }
-
-    @Test
-    fun unseenFilter_headsUpMarkedAsSeen() {
-        // GIVEN: Keyguard is not showing, shade is not expanded
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(false)
-        runKeyguardCoordinatorTest {
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Gone),
-                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
-            )
-
-            // WHEN: A notification is posted
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: That notification is heads up
-            onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
-            testScheduler.runCurrent()
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Lockscreen),
-                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
-            )
-
-            // THEN: The notification is recognized as "seen" and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
-            // WHEN: The keyguard goes away
-            keyguardRepository.setKeyguardShowing(false)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Gone),
-                stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE)
-            )
-
-            // THEN: The notification is shown regardless
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
-        // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            val fakeEntry =
-                NotificationEntryBuilder()
-                    .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build())
-                    .build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is recognized as "ongoing" and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
-        // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            val fakeEntry =
-                NotificationEntryBuilder().build().apply {
-                    row =
-                        mock<ExpandableNotificationRow>().apply {
-                            whenever(isMediaRow).thenReturn(true)
-                        }
-                }
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is recognized as "media" and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
-        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is recognized as "seen" and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-
-            // WHEN: The filter is cleaned up
-            unseenFilter.onCleanup()
-
-            // THEN: The SeenNotificationProvider has been updated to reflect the suppression
-            assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue()
-        }
-    }
-
-    @Test
-    fun unseenFilterInvalidatesWhenSettingChanges() {
-        // GIVEN: Keyguard is not showing, and shade is expanded
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            // GIVEN: A notification is present
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // GIVEN: The setting for filtering unseen notifications is disabled
-            showOnlyUnseenNotifsOnKeyguardSetting = false
-
-            // GIVEN: The pipeline has registered the unseen filter for invalidation
-            val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock()
-            unseenFilter.setInvalidationListener(invalidationListener)
-
-            // WHEN: The keyguard is now showing
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is not filtered out
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-
-            // WHEN: The secure setting is changed
-            showOnlyUnseenNotifsOnKeyguardSetting = true
-
-            // THEN: The pipeline is invalidated
-            verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString())
-
-            // THEN: The notification is recognized as "seen" and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-        }
-    }
-
-    @Test
-    fun unseenFilterAllowsNewNotif() {
-        // GIVEN: Keyguard is showing, no notifications present
-        keyguardRepository.setKeyguardShowing(true)
-        runKeyguardCoordinatorTest {
-            // WHEN: A new notification is posted
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // THEN: The notification is recognized as "unseen" and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenFilterSeenGroupSummaryWithUnseenChild() {
-        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
-        keyguardRepository.setKeyguardShowing(false)
-        whenever(statusBarStateController.isExpanded).thenReturn(true)
-        runKeyguardCoordinatorTest {
-            // WHEN: A new notification is posted
-            val fakeSummary = NotificationEntryBuilder().build()
-            val fakeChild =
-                NotificationEntryBuilder()
-                    .setGroup(context, "group")
-                    .setGroupSummary(context, false)
-                    .build()
-            GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build()
-
-            collectionListener.onEntryAdded(fakeSummary)
-            collectionListener.onEntryAdded(fakeChild)
-
-            // WHEN: Keyguard is now showing, both notifications are marked as seen
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // WHEN: The child notification is now unseen
-            collectionListener.onEntryUpdated(fakeChild)
-
-            // THEN: The summary is not filtered out, because the child is unseen
-            assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() {
-        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
-        keyguardRepository.setKeyguardShowing(true)
-        keyguardRepository.setIsDozing(false)
-        runKeyguardCoordinatorTest {
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.AOD,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: five seconds have passed
-            testScheduler.advanceTimeBy(5.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: Keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Gone),
-                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
-            )
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Lockscreen),
-                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD)
-            )
-
-            // THEN: The notification is now recognized as "seen" and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
-        }
-    }
-
-    @Test
-    fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
-        // GIVEN: Keyguard is showing, unseen notification is present
-        keyguardRepository.setKeyguardShowing(true)
-        runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            val fakeEntry = NotificationEntryBuilder().build()
-            collectionListener.onEntryAdded(fakeEntry)
-
-            // WHEN: Keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.GONE,
-                this.testScheduler,
-            )
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            testScheduler.runCurrent()
-
-            // THEN: The notification is not recognized as "seen" and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() {
-        // GIVEN: Keyguard is showing, not dozing, unseen notification is present
-        keyguardRepository.setKeyguardShowing(true)
-        keyguardRepository.setIsDozing(false)
-        runKeyguardCoordinatorTest {
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Lockscreen),
-                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
-            )
-            val firstEntry = NotificationEntryBuilder().setId(1).build()
-            collectionListener.onEntryAdded(firstEntry)
-            testScheduler.runCurrent()
-
-            // WHEN: one second has passed
-            testScheduler.advanceTimeBy(1.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: another unseen notification is posted
-            val secondEntry = NotificationEntryBuilder().setId(2).build()
-            collectionListener.onEntryAdded(secondEntry)
-            testScheduler.runCurrent()
-
-            // WHEN: four more seconds have passed
-            testScheduler.advanceTimeBy(4.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Gone),
-                stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
-            )
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            kosmos.setTransition(
-                sceneTransition = Idle(Scenes.Lockscreen),
-                stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
-            )
-
-            // THEN: The first notification is considered seen and is filtered out.
-            assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue()
-
-            // THEN: The second notification is still considered unseen and is not filtered out
-            assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() {
-        // GIVEN: Keyguard is showing, not dozing
-        keyguardRepository.setKeyguardShowing(true)
-        keyguardRepository.setIsDozing(false)
-        runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: a new notification is posted
-            val entry = NotificationEntryBuilder().setId(1).build()
-            collectionListener.onEntryAdded(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: five more seconds have passed
-            testScheduler.advanceTimeBy(5.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the notification is removed
-            collectionListener.onEntryRemoved(entry, 0)
-            testScheduler.runCurrent()
-
-            // WHEN: the notification is re-posted
-            collectionListener.onEntryAdded(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: one more second has passed
-            testScheduler.advanceTimeBy(1.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.GONE,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // THEN: The notification is considered unseen and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() {
-        // GIVEN: Keyguard is showing, not dozing
-        keyguardRepository.setKeyguardShowing(true)
-        keyguardRepository.setIsDozing(false)
-        runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: a new notification is posted
-            val entry = NotificationEntryBuilder().setId(1).build()
-            collectionListener.onEntryAdded(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: one second has passed
-            testScheduler.advanceTimeBy(1.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the notification is removed
-            collectionListener.onEntryRemoved(entry, 0)
-            testScheduler.runCurrent()
-
-            // WHEN: the notification is re-posted
-            collectionListener.onEntryAdded(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: one more second has passed
-            testScheduler.advanceTimeBy(1.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.GONE,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // THEN: The notification is considered unseen and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
-        }
-    }
-
-    @Test
-    fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() {
-        // GIVEN: Keyguard is showing, not dozing
-        keyguardRepository.setKeyguardShowing(true)
-        keyguardRepository.setIsDozing(false)
-        runKeyguardCoordinatorTest {
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: a new notification is posted
-            val entry = NotificationEntryBuilder().setId(1).build()
-            collectionListener.onEntryAdded(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: one second has passed
-            testScheduler.advanceTimeBy(1.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the notification is updated
-            collectionListener.onEntryUpdated(entry)
-            testScheduler.runCurrent()
-
-            // WHEN: four more seconds have passed
-            testScheduler.advanceTimeBy(4.seconds)
-            testScheduler.runCurrent()
-
-            // WHEN: the keyguard is no longer showing
-            keyguardRepository.setKeyguardShowing(false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.GONE,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // WHEN: Keyguard is shown again
-            keyguardRepository.setKeyguardShowing(true)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.GONE,
-                to = KeyguardState.LOCKSCREEN,
-                this.testScheduler,
-            )
-            testScheduler.runCurrent()
-
-            // THEN: The notification is considered unseen and is not filtered out.
-            assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse()
-        }
-    }
-
-    private fun runKeyguardCoordinatorTest(
-        testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit
-    ) {
-        val testDispatcher = UnconfinedTestDispatcher()
-        val testScope = TestScope(testDispatcher)
-        val fakeSettings =
-            FakeSettings().apply {
-                putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
-            }
-        val seenNotificationsInteractor =
-            SeenNotificationsInteractor(ActiveNotificationListRepository())
-        val keyguardCoordinator =
-            KeyguardCoordinator(
-                testDispatcher,
-                mock<DumpManager>(),
-                headsUpManager,
-                keyguardNotifVisibilityProvider,
-                keyguardRepository,
-                kosmos.keyguardTransitionInteractor,
-                KeyguardCoordinatorLogger(logcatLogBuffer()),
-                testScope.backgroundScope,
-                sectionHeaderVisibilityProvider,
-                fakeSettings,
-                seenNotificationsInteractor,
-                statusBarStateController,
-            )
-        keyguardCoordinator.attach(notifPipeline)
-        testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
-            KeyguardCoordinatorTestScope(
-                    keyguardCoordinator,
-                    testScope,
-                    seenNotificationsInteractor,
-                    fakeSettings,
-                )
-                .testBlock()
-        }
-    }
-
-    private inner class KeyguardCoordinatorTestScope(
-        private val keyguardCoordinator: KeyguardCoordinator,
-        private val scope: TestScope,
-        val seenNotificationsInteractor: SeenNotificationsInteractor,
-        private val fakeSettings: FakeSettings,
-    ) : CoroutineScope by scope {
-        val testScheduler: TestCoroutineScheduler
-            get() = scope.testScheduler
-
-        val onStateChangeListener: Consumer<String> = withArgCaptor {
-            verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
-        }
-
-        val unseenFilter: NotifFilter
-            get() = keyguardCoordinator.unseenNotifFilter
-
-        val collectionListener: NotifCollectionListener = withArgCaptor {
-            verify(notifPipeline).addCollectionListener(capture())
-        }
-
-        val onHeadsUpChangedListener: OnHeadsUpChangedListener
-            get() = withArgCaptor { verify(headsUpManager).addListener(capture()) }
-
-        val statusBarStateListener: StatusBarStateController.StateListener
-            get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
-
-        var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
-            get() =
-                fakeSettings.getIntForUser(
-                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    UserHandle.USER_CURRENT,
-                ) == 1
-            set(value) {
-                fakeSettings.putIntForUser(
-                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    if (value) 1 else 2,
-                    UserHandle.USER_CURRENT,
-                )
-            }
-    }
-
-    companion object {
-        @JvmStatic
-        @Parameters(name = "{0}")
-        fun getParams(): List<FlagsParameterization> {
-            return FlagsParameterization.allCombinationsOf().andSceneContainer()
-        }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index a925ccf..b799595 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -91,6 +91,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -1196,6 +1197,26 @@
     }
 
     @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    public void testGenerateHeadsUpAnimation_isSeenInShade_noAnimation() {
+        // GIVEN NSSL is ready for HUN animations
+        Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class);
+        prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener);
+
+        // Entry was seen in shade
+        NotificationEntry entry = mock(NotificationEntry.class);
+        when(entry.isSeenInShade()).thenReturn(true);
+        ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
+        when(row.getEntry()).thenReturn(entry);
+
+        // WHEN we generate an add event
+        mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true);
+
+        // THEN nothing happens
+        assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse();
+    }
+
+    @Test
     @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
     public void testOnChildAnimationsFinished_resetsheadsUpAnimatingAway() {
         // GIVEN NSSL is ready for HUN animations
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index c2a7b52..b7ebebe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -126,6 +126,19 @@
     }
 
     @Test
+    @EnableSceneContainer
+    fun resetViewStates_defaultHun_yTranslationIsHeadsUpTop() {
+        val headsUpTop = 200f
+        ambientState.headsUpTop = headsUpTop
+
+        whenever(notificationRow.isPinned).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(headsUpTop)
+    }
+
+    @Test
+    @DisableSceneContainer
     fun resetViewStates_defaultHun_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -133,6 +146,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     fun resetViewStates_defaultHunWithStackMargin_changesHunYTranslation() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -140,7 +154,7 @@
     }
 
     @Test
-    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+    @DisableSceneContainer
     fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
         whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -153,6 +167,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_yTranslationIsInset() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -160,6 +175,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -167,6 +183,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
@@ -175,6 +192,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() {
         whenever(notificationRow.isPinned).thenReturn(true)
@@ -183,7 +201,98 @@
     }
 
     @Test
-    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+    @EnableSceneContainer
+    fun resetViewStates_defaultHunInShade_stackTopEqualsHunTop_hunHasFullHeight() {
+        // Given: headsUpTop == stackTop -> haven't scrolled the stack yet
+        val headsUpTop = 150f
+        val collapsedHeight = 100
+        val intrinsicHeight = 300
+        fakeHunInShade(
+            headsUpTop = headsUpTop,
+            stackTop = headsUpTop,
+            collapsedHeight = collapsedHeight,
+            intrinsicHeight = intrinsicHeight,
+        )
+
+        // When
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        // Then: HUN is at the headsUpTop
+        assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+        // And: HUN has its full height
+        assertThat(notificationRow.viewState.height).isEqualTo(intrinsicHeight)
+    }
+
+    @Test
+    @EnableSceneContainer
+    fun resetViewStates_defaultHunInShade_stackTopGreaterThanHeadsUpTop_hunClampedToHeadsUpTop() {
+        // Given: headsUpTop < stackTop -> scrolled the stack a little bit
+        val stackTop = -25f
+        val headsUpTop = 150f
+        val collapsedHeight = 100
+        val intrinsicHeight = 300
+        fakeHunInShade(
+            headsUpTop = headsUpTop,
+            stackTop = stackTop,
+            collapsedHeight = collapsedHeight,
+            intrinsicHeight = intrinsicHeight
+        )
+
+        // When
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        // Then: HUN is translated to the headsUpTop
+        assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+        // And: HUN is clipped to the available space
+        // newTranslation = max(150, -25)
+        // distToReal = 150 - (-25)
+        // height = max(300 - 175, 100)
+        assertThat(notificationRow.viewState.height).isEqualTo(125)
+    }
+
+    @Test
+    @EnableSceneContainer
+    fun resetViewStates_defaultHunInShade_stackOverscrolledHun_hunClampedToHeadsUpTop() {
+        // Given: headsUpTop << stackTop -> stack has fully overscrolled the HUN
+        val stackTop = -500f
+        val headsUpTop = 150f
+        val collapsedHeight = 100
+        val intrinsicHeight = 300
+        fakeHunInShade(
+            headsUpTop = headsUpTop,
+            stackTop = stackTop,
+            collapsedHeight = collapsedHeight,
+            intrinsicHeight = intrinsicHeight
+        )
+
+        // When
+        stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+        // Then: HUN is translated to the headsUpTop
+        assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+        // And: HUN is clipped to its collapsed height
+        assertThat(notificationRow.viewState.height).isEqualTo(collapsedHeight)
+    }
+
+    @Test
+    @EnableSceneContainer
+    fun resetViewStates_defaultHun_showingQS_hunTranslatedToHeadsUpTop() {
+        // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+        val headsUpTop = 2000f
+        fakeHunInShade(
+            headsUpTop = headsUpTop,
+            stackTop = 2600f, // stack scrolled below the screen
+            stackCutoff = 4000f,
+            collapsedHeight = 100,
+            intrinsicHeight = 300
+        )
+        whenever(notificationRow.isAboveShelf).thenReturn(true)
+
+        resetViewStates_hunYTranslationIs(headsUpTop)
+    }
+
+    @Test
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
         // Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -200,7 +309,7 @@
     }
 
     @Test
-    @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
         // Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -245,6 +354,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAwayWhileDozing_yTranslationIsInset() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -255,6 +365,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
     fun resetViewStates_hunAnimatingAwayWhileDozing_hasStackMargin_changesHunYTranslation() {
         whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -744,8 +855,7 @@
         expandableViewState.yTranslation = 50f
 
         stackScrollAlgorithm.clampHunToTop(
-            /* quickQsOffsetHeight= */ 10f,
-            /* stackTranslation= */ 0f,
+            /* headsUpTop= */ 10f,
             /* collapsedHeight= */ 1f,
             expandableViewState
         )
@@ -760,8 +870,7 @@
         expandableViewState.yTranslation = -10f
 
         stackScrollAlgorithm.clampHunToTop(
-            /* quickQsOffsetHeight= */ 10f,
-            /* stackTranslation= */ 0f,
+            /* headsUpTop= */ 10f,
             /* collapsedHeight= */ 1f,
             expandableViewState
         )
@@ -777,8 +886,7 @@
         expandableViewState.yTranslation = -100f
 
         stackScrollAlgorithm.clampHunToTop(
-            /* quickQsOffsetHeight= */ 10f,
-            /* stackTranslation= */ 0f,
+            /* headsUpTop= */ 10f,
             /* collapsedHeight= */ 10f,
             expandableViewState
         )
@@ -796,8 +904,7 @@
         expandableViewState.yTranslation = 5f
 
         stackScrollAlgorithm.clampHunToTop(
-            /* quickQsOffsetHeight= */ 10f,
-            /* stackTranslation= */ 0f,
+            /* headsUpTop= */ 10f,
             /* collapsedHeight= */ 10f,
             expandableViewState
         )
@@ -1309,6 +1416,33 @@
 
         expect.that(notificationRow.viewState.alpha).isEqualTo(expectedAlpha)
     }
+
+    /** fakes the notification row under test, to be a HUN in a fully opened shade */
+    private fun fakeHunInShade(
+        headsUpTop: Float,
+        collapsedHeight: Int,
+        intrinsicHeight: Int,
+        stackTop: Float,
+        stackCutoff: Float = 2000f,
+        fullStackHeight: Float = 3000f
+    ) {
+        ambientState.headsUpTop = headsUpTop
+        ambientState.stackTop = stackTop
+        ambientState.stackCutoff = stackCutoff
+
+        // shade is fully open
+        ambientState.expansionFraction = 1.0f
+        with(fullStackHeight) {
+            ambientState.stackHeight = this
+            ambientState.stackEndHeight = this
+        }
+        stackScrollAlgorithm.setIsExpanded(true)
+
+        whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+        whenever(notificationRow.isHeadsUp).thenReturn(true)
+        whenever(notificationRow.collapsedHeight).thenReturn(collapsedHeight)
+        whenever(notificationRow.intrinsicHeight).thenReturn(intrinsicHeight)
+    }
 }
 
 private fun mockExpandableNotificationRow(): ExpandableNotificationRow {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index fea0e72..8dfbb37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -118,8 +118,8 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     public void testAppearResetsTranslation() {
-        mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
         mController.setupAodIcons(mAodIcons);
         when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
         mController.appearAodIcons();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 49e3f04..31f93b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1059,6 +1059,7 @@
 
     @Test
     @DisableSceneContainer
+    @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
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 01540e7..58ad835 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
@@ -536,7 +536,7 @@
 
         // WHEN there's *no* ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false);
+                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
 
         // THEN the old callback value is used, so the view is shown
         assertEquals(View.VISIBLE,
@@ -548,7 +548,7 @@
 
         // WHEN there *is* an ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         // THEN the old callback value is used, so the view is hidden
         assertEquals(View.GONE,
@@ -565,7 +565,7 @@
         // listener, but I'm unable to get the fragment to get attached so that the binder starts
         // listening to flows.
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false);
+                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
 
         assertEquals(View.GONE,
                 mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -577,7 +577,7 @@
         resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         assertEquals(View.VISIBLE,
                 mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -590,7 +590,7 @@
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         fragment.disable(DEFAULT_DISPLAY,
                 StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
@@ -605,7 +605,7 @@
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
 
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
         when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
@@ -621,14 +621,14 @@
 
         // Ongoing activity started
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         assertEquals(View.VISIBLE,
                 mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
 
         // Ongoing activity ended
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false);
+                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
 
         assertEquals(View.GONE,
                 mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility());
@@ -643,7 +643,7 @@
 
         // Ongoing call started
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         // Notification area is hidden without delay
         assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01);
@@ -661,7 +661,7 @@
 
         // WHEN there's *no* ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ false);
+                /* hasOngoingActivity= */ false, /* shouldAnimate= */ false);
 
         // THEN the new callback value is used, so the view is hidden
         assertEquals(View.GONE,
@@ -673,7 +673,7 @@
 
         // WHEN there *is* an ongoing activity via new callback
         mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
-                /* hasOngoingActivity= */ true);
+                /* hasOngoingActivity= */ true, /* shouldAnimate= */ false);
 
         // THEN the new callback value is used, so the view is shown
         assertEquals(View.VISIBLE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 94159bc..60750cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -425,7 +425,7 @@
 
             kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
 
-            assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
 
             kosmos.fakeMediaProjectionRepository.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
index d3f1125..cefdf7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
@@ -29,7 +29,7 @@
     override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>()
 
     override val ongoingActivityChip: MutableStateFlow<OngoingActivityChipModel> =
-        MutableStateFlow(OngoingActivityChipModel.Hidden)
+        MutableStateFlow(OngoingActivityChipModel.Hidden())
 
     override val isHomeStatusBarAllowedByScene = MutableStateFlow(false)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cc2ef53..12cfdcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.batteryController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
index f40282f..a8271fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt
@@ -21,10 +21,9 @@
 import android.view.MotionEvent
 import android.view.MotionEvent.CLASSIFICATION_NONE
 import android.view.MotionEvent.TOOL_TYPE_FINGER
-import java.lang.reflect.Method
-import org.mockito.kotlin.doNothing
+import org.mockito.AdditionalAnswers
+import org.mockito.Mockito.mock
 import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
 
 fun motionEvent(
@@ -40,31 +39,18 @@
     val event =
         MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0)
     event.source = source
-    val spy =
-        spy<MotionEvent>(event) {
-            on { getToolType(0) } doReturn toolType
-            on { getPointerCount() } doReturn pointerCount
-            axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value }
-            on { getClassification() } doReturn classification
-        }
-    ensureFinalizeIsNotCalledTwice(spy)
-    return spy
-}
-
-private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) {
-    // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means
-    // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type
-    // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize()
-    // method which goes to native code and tries to delete the reference from mNativePtr. It works
-    // first time but second time reference is already deleted and it breaks. That's why we have to
-    // avoid calling finalize twice
-    doNothing().whenever(spy).finalizeUsingReflection()
-}
-
-private fun MotionEvent.finalizeUsingReflection() {
-    val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize")
-    finalizeMethod.isAccessible = true
-    finalizeMethod.invoke(this)
+    // we need to use mock with delegation instead of spy because:
+    // 1. Spy will try to deallocate the same memory again when finalize() is called as it keep the
+    // same memory pointer to native MotionEvent
+    // 2. Even after workaround for issue above there still remains problem with destructor of
+    // native event trying to free the same chunk of native memory. I'm not sure why it happens but
+    // mock seems to fix the issue and because it delegates all calls seems safer overall
+    val delegate = mock(MotionEvent::class.java, AdditionalAnswers.delegatesTo<MotionEvent>(event))
+    doReturn(toolType).whenever(delegate).getToolType(0)
+    doReturn(pointerCount).whenever(delegate).pointerCount
+    doReturn(classification).whenever(delegate).classification
+    axisValues.forEach { (key, value) -> doReturn(value).whenever(delegate).getAxisValue(key) }
+    return delegate
 }
 
 fun touchpadEvent(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f737148..3f5dc82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -37,16 +39,19 @@
 import android.media.session.MediaSession;
 import android.os.Handler;
 import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
 import android.testing.TestableLooper;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.settingslib.flags.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
@@ -54,7 +59,9 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.concurrency.FakeThreadFactory;
 import com.android.systemui.util.concurrency.ThreadFactory;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.volume.domain.interactor.AudioSharingInteractor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -63,6 +70,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 @RunWith(AndroidJUnit4.class)
@@ -104,6 +112,10 @@
     private UserTracker mUserTracker;
     @Mock
     private DumpManager mDumpManager;
+    @Mock
+    private AudioSharingInteractor mAudioSharingInteractor;
+    @Mock
+    private JavaAdapter mJavaAdapter;
 
 
     @Before
@@ -124,11 +136,26 @@
 
         mCallback = mock(VolumeDialogControllerImpl.C.class);
         mThreadFactory.setLooper(TestableLooper.get(this).getLooper());
-        mVolumeController = new TestableVolumeDialogControllerImpl(mContext,
-                mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager,
-                mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager,
-                mPackageManager, mWakefullnessLifcycle, mKeyguardManager,
-                mActivityManager, mUserTracker, mDumpManager, mCallback);
+        mVolumeController =
+                new TestableVolumeDialogControllerImpl(
+                        mContext,
+                        mBroadcastDispatcher,
+                        mRingerModeTracker,
+                        mThreadFactory,
+                        mAudioManager,
+                        mNotificationManager,
+                        mVibrator,
+                        mIAudioService,
+                        mAccessibilityManager,
+                        mPackageManager,
+                        mWakefullnessLifcycle,
+                        mKeyguardManager,
+                        mActivityManager,
+                        mUserTracker,
+                        mDumpManager,
+                        mCallback,
+                        mAudioSharingInteractor,
+                        mJavaAdapter);
         mVolumeController.setEnableDialogs(true, true);
     }
 
@@ -224,6 +251,41 @@
         verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class));
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void handleAudioSharingStreamVolumeChanges_updateState() {
+        ArgumentCaptor<VolumeDialogController.State> stateCaptor =
+                ArgumentCaptor.forClass(VolumeDialogController.State.class);
+        int broadcastStream = VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(100);
+
+        verify(mCallback).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue();
+        assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(100);
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(200);
+
+        verify(mCallback, times(2)).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue();
+        assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(200);
+
+        mVolumeController.handleAudioSharingStreamVolumeChanges(null);
+
+        verify(mCallback, times(3)).onStateChanged(stateCaptor.capture());
+        assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void testSetStreamVolume_setSecondaryDeviceVolume() {
+        mVolumeController.setStreamVolume(
+                VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST, /* level= */ 100);
+        Objects.requireNonNull(TestableLooper.get(this)).processAllMessages();
+
+        verify(mAudioSharingInteractor).setStreamVolume(100);
+    }
+
     static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl {
         private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver;
 
@@ -243,11 +305,27 @@
                 ActivityManager activityManager,
                 UserTracker userTracker,
                 DumpManager dumpManager,
-                C callback) {
-            super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager,
-                    notificationManager, optionalVibrator, iAudioService, accessibilityManager,
-                    packageManager, wakefulnessLifecycle, keyguardManager,
-                    activityManager, userTracker, dumpManager);
+                C callback,
+                AudioSharingInteractor audioSharingInteractor,
+                JavaAdapter javaAdapter) {
+            super(
+                    context,
+                    broadcastDispatcher,
+                    ringerModeTracker,
+                    theadFactory,
+                    audioManager,
+                    notificationManager,
+                    optionalVibrator,
+                    iAudioService,
+                    accessibilityManager,
+                    packageManager,
+                    wakefulnessLifecycle,
+                    keyguardManager,
+                    activityManager,
+                    userTracker,
+                    dumpManager,
+                    audioSharingInteractor,
+                    javaAdapter);
             mCallbacks = callback;
 
             ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index cdfcca6..b5cbf59 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -23,6 +23,7 @@
 import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER;
 import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN;
 import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN;
+import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST;
 import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS;
 
 import static junit.framework.Assert.assertEquals;
@@ -72,6 +73,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.settingslib.flags.Flags;
 import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
@@ -794,6 +796,38 @@
         verify(mVolumeDialogInteractor, atLeastOnce()).onDialogDismissed(); // dismiss by timeout
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX)
+    public void testDynamicStreamForBroadcast_createRow() {
+        State state = createShellState();
+        VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState();
+        ss.dynamic = true;
+        ss.levelMin = 0;
+        ss.levelMax = 255;
+        ss.level = 20;
+        ss.name = -1;
+        ss.remoteLabel = mContext.getString(R.string.audio_sharing_description);
+        state.states.append(DYNAMIC_STREAM_BROADCAST, ss);
+
+        mDialog.onStateChangedH(state);
+        mTestableLooper.processAllMessages();
+
+        ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows);
+        assumeNotNull(volumeDialogRows);
+        View broadcastRow = null;
+        final int rowCount = volumeDialogRows.getChildCount();
+        // we don't make assumptions about the position of the dnd row
+        for (int i = 0; i < rowCount; i++) {
+            View volumeRow = volumeDialogRows.getChildAt(i);
+            if (volumeRow.getId() == DYNAMIC_STREAM_BROADCAST) {
+                broadcastRow = volumeRow;
+                break;
+            }
+        }
+        assertNotNull(broadcastRow);
+        assertEquals(broadcastRow.getVisibility(), View.VISIBLE);
+    }
+
     /**
      * @return true if at least one volume row has the DND icon
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index ac42319..60b5b5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -86,6 +86,7 @@
 import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
 import android.testing.TestableLooper;
+import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.Display;
@@ -183,6 +184,7 @@
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -192,7 +194,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -216,6 +217,9 @@
 @RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class BubblesTest extends SysuiTestCase {
+
+    private static final String TAG = "BubblesTest";
+
     @Mock
     private CommonNotifCollection mCommonNotifCollection;
     @Mock
@@ -241,8 +245,6 @@
     @Mock
     private KeyguardBypassController mKeyguardBypassController;
     @Mock
-    private FloatingContentCoordinator mFloatingContentCoordinator;
-    @Mock
     private BubbleDataRepository mDataRepository;
     @Mock
     private NotificationShadeWindowView mNotificationShadeWindowView;
@@ -372,6 +374,7 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        PhysicsAnimatorTestUtils.prepareForTest();
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             doReturn(true).when(mTransitions).isRegistered();
@@ -494,7 +497,7 @@
                 mShellCommandHandler,
                 mShellController,
                 mBubbleData,
-                mFloatingContentCoordinator,
+                new FloatingContentCoordinator(),
                 mDataRepository,
                 mStatusBarService,
                 mWindowManager,
@@ -571,12 +574,32 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
         for (int i = 0; i < bubbles.size(); i++) {
             mBubbleController.removeBubble(bubbles.get(i).getKey(),
                     Bubbles.DISMISS_NO_LONGER_BUBBLE);
         }
+        mTestableLooper.processAllMessages();
+
+        // check that no animations are running before finishing the test to make sure that the
+        // state gets cleaned up correctly between tests.
+        int retryCount = 0;
+        while (PhysicsAnimatorTestUtils.isAnyAnimationRunning() && retryCount <= 10) {
+            Log.d(
+                    TAG,
+                    String.format("waiting for animations to complete. attempt %d", retryCount));
+            // post a message to the looper and wait for it to be processed
+            mTestableLooper.runWithLooper(() -> {});
+            retryCount++;
+        }
+        mTestableLooper.processAllMessages();
+        if (PhysicsAnimatorTestUtils.isAnyAnimationRunning()) {
+            Log.d(TAG, "finished waiting for animations to complete but animations are still "
+                    + "running");
+        } else {
+            Log.d(TAG, "no animations are running");
+        }
     }
 
     @Test
@@ -1853,7 +1876,6 @@
                 any(Bubble.class), anyBoolean(), anyBoolean());
     }
 
-    @Ignore("reason = b/351977103")
     @Test
     public void testShowStackEdu_isNotConversationBubble() {
         // Setup
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt
new file mode 100644
index 0000000..2050437
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.communalSceneTransitionRepository: CommunalSceneTransitionRepository by
+    Kosmos.Fixture { CommunalSceneTransitionRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
index 1da1fb2..5e870b1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
@@ -25,20 +25,11 @@
 /** Fake implementation of [CommunalPrefsRepository] */
 class FakeCommunalPrefsRepository : CommunalPrefsRepository {
     private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet())
-    private val _isDisclaimerDismissed = MutableStateFlow<Set<UserInfo>>(emptySet())
 
     override fun isCtaDismissed(user: UserInfo): Flow<Boolean> =
         _isCtaDismissed.map { it.contains(user) }
 
-    override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> =
-        _isDisclaimerDismissed.map { it.contains(user) }
-
     override suspend fun setCtaDismissed(user: UserInfo) {
         _isCtaDismissed.value = _isCtaDismissed.value.toMutableSet().apply { add(user) }
     }
-
-    override suspend fun setDisclaimerDismissed(user: UserInfo) {
-        _isDisclaimerDismissed.value =
-            _isDisclaimerDismissed.value.toMutableSet().apply { add(user) }
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
index d280be2..8245481 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
@@ -6,7 +6,6 @@
 import com.android.systemui.communal.shared.model.CommunalScenes
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -25,11 +24,10 @@
 ) : CommunalSceneRepository {
 
     override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) =
-        snapToScene(toScene, 0)
+        snapToScene(toScene)
 
-    override fun snapToScene(toScene: SceneKey, delayMillis: Long) {
+    override fun snapToScene(toScene: SceneKey) {
         applicationScope.launch {
-            delay(delayMillis)
             currentScene.value = toScene
             _transitionState.value = flowOf(ObservableTransitionState.Idle(toScene))
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index eb92785..4ad046c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -42,6 +43,7 @@
     CommunalInteractor(
         applicationScope = applicationCoroutineScope,
         bgDispatcher = testDispatcher,
+        bgScope = testScope.backgroundScope,
         broadcastDispatcher = broadcastDispatcher,
         communalSceneInteractor = communalSceneInteractor,
         widgetRepository = communalWidgetRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
new file mode 100644
index 0000000..e6e59e1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
@@ -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 com.android.systemui.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.communalSceneTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.internalKeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by
+    Kosmos.Fixture {
+        CommunalSceneTransitionInteractor(
+            applicationScope = applicationCoroutineScope,
+            transitionInteractor = keyguardTransitionInteractor,
+            internalTransitionInteractor = internalKeyguardTransitionInteractor,
+            settingsInteractor = communalSettingsInteractor,
+            sceneInteractor = communalSceneInteractor,
+            repository = communalSceneTransitionRepository,
+            keyguardInteractor = keyguardInteractor,
+        )
+    }
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 045bd5d..2dcd275 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
@@ -21,21 +21,32 @@
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 /** Fake implementation of [DeviceEntryRepository] */
 @SysUISingleton
 class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository {
-    private var isLockscreenEnabled = true
+
+    private val _isLockscreenEnabled = MutableStateFlow(true)
+    override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow()
 
     private val _isBypassEnabled = MutableStateFlow(false)
     override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
 
+    private var pendingLockscreenEnabled = _isLockscreenEnabled.value
+
     override suspend fun isLockscreenEnabled(): Boolean {
-        return isLockscreenEnabled
+        _isLockscreenEnabled.value = pendingLockscreenEnabled
+        return isLockscreenEnabled.value
     }
 
     fun setLockscreenEnabled(isLockscreenEnabled: Boolean) {
-        this.isLockscreenEnabled = isLockscreenEnabled
+        _isLockscreenEnabled.value = isLockscreenEnabled
+        pendingLockscreenEnabled = _isLockscreenEnabled.value
+    }
+
+    fun setPendingLockscreenEnabled(isLockscreenEnabled: Boolean) {
+        pendingLockscreenEnabled = isLockscreenEnabled
     }
 
     fun setBypassEnabled(isBypassEnabled: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index f73f43d..edf4bcc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.education.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import java.time.Clock
 import java.time.Instant
 
 var Kosmos.contextualEducationRepository: ContextualEducationRepository by
-    Kosmos.Fixture { FakeContextualEducationRepository(FakeEduClock(Instant.MIN)) }
+    Kosmos.Fixture { FakeContextualEducationRepository(fakeEduClock) }
+
+var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
new file mode 100644
index 0000000..5b2dc2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.systemui.education.domain.interactor
+
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.contextualEducationInteractor by
+    Kosmos.Fixture {
+        ContextualEducationInteractor(
+            backgroundScope = testScope.backgroundScope,
+            repository = contextualEducationRepository,
+            selectedUserInteractor = selectedUserInteractor
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
new file mode 100644
index 0000000..8f84e04
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 com.android.systemui.education.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+
+var Kosmos.keyboardTouchpadEduStatsInteractor by
+    Kosmos.Fixture {
+        KeyboardTouchpadEduStatsInteractorImpl(
+            backgroundScope = testScope.backgroundScope,
+            contextualEducationInteractor = contextualEducationInteractor
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index 126d858..4634a7f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -19,7 +19,7 @@
 import android.service.dream.dreamManager
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.communalSceneInteractor
-import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -41,7 +41,7 @@
             communalSceneInteractor = communalSceneInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
-            deviceEntryRepository = deviceEntryRepository,
+            deviceEntryInteractor = deviceEntryInteractor,
             wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
             dreamManager = dreamManager
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
index 3d85a4a..c7dfd5c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
 
 import android.content.applicationContext
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor
@@ -34,6 +36,7 @@
             mediaRouterChipInteractor = mediaRouterChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
             logger = statusBarChipsLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
index 1ed7a47..651a0f7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.mediaprojection.ui.view
 
 import android.content.packageManager
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 
@@ -24,6 +25,7 @@
     Kosmos.Fixture {
         EndMediaProjectionDialogHelper(
             dialogFactory = mockSystemUIDialogFactory,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
             packageManager = packageManager,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
index e4bb166..c2a6f7d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt
@@ -17,10 +17,12 @@
 package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel
 
 import android.content.applicationContext
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
 import com.android.systemui.statusbar.chips.statusBarChipsLogger
 import com.android.systemui.util.time.fakeSystemClock
 
@@ -30,7 +32,9 @@
             scope = applicationCoroutineScope,
             context = applicationContext,
             interactor = screenRecordChipInteractor,
+            shareToAppChipViewModel = shareToAppChipViewModel,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
             systemClock = fakeSystemClock,
             logger = statusBarChipsLogger,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
index 8ed7f96..0770009 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
 
 import android.content.applicationContext
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
@@ -32,6 +33,7 @@
             mediaProjectionChipInteractor = mediaProjectionChipInteractor,
             systemClock = fakeSystemClock,
             endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
             logger = statusBarChipsLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index da956ec..8b4de2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -22,13 +22,12 @@
  * The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any
  * modifications made to the builder will be applied to the [entry].
  */
-inline fun modifyEntry(
-    entry: NotificationEntry,
+inline fun NotificationEntry.modifyEntry(
     crossinline modifier: NotificationEntryBuilder.() -> Unit
 ) {
-    val builder = NotificationEntryBuilder(entry)
+    val builder = NotificationEntryBuilder(this)
     modifier(builder)
-    builder.apply(entry)
+    builder.apply(this)
 }
 
 fun getAttachState(entry: ListEntry): ListAttachState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
new file mode 100644
index 0000000..77d97bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.settings.fakeSettings
+
+var Kosmos.lockScreenMinimalismCoordinator by
+    Kosmos.Fixture {
+        LockScreenMinimalismCoordinator(
+            bgDispatcher = testDispatcher,
+            dumpManager = dumpManager,
+            headsUpInteractor = headsUpNotificationInteractor,
+            logger = lockScreenMinimalismCoordinatorLogger,
+            scope = testScope.backgroundScope,
+            secureSettings = fakeSettings,
+            seenNotificationsInteractor = seenNotificationsInteractor,
+            statusBarStateController = statusBarStateController,
+            shadeInteractor = shadeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
new file mode 100644
index 0000000..77aeb44
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.statusbar.notification.collection.coordinator
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.lockScreenMinimalismCoordinatorLogger by
+    Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 492e87b..7e8f1a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -22,14 +22,19 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 
 val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
 
 class FakeHeadsUpNotificationRepository : HeadsUpRepository {
     override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
-    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
-    override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
-        MutableStateFlow(emptySet())
+
+    val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>())
+    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> =
+        orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged()
+    override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> =
+        orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged()
 
     override fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         isHeadsUpAnimatingAway.value = animatingAway
@@ -38,4 +43,12 @@
     override fun snooze() {
         // do nothing
     }
+
+    fun setNotifications(notifications: List<HeadsUpRowRepository>) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
+
+    fun setNotifications(vararg notifications: HeadsUpRowRepository) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
deleted file mode 100644
index 9be7dfe..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.stack.data.repository
-
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
-
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
-    setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
-    this.activeHeadsUpRows.value = notifications.toSet()
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index 9247e88..e3176f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -26,9 +26,14 @@
 class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
     private val _isDeviceProvisioned = MutableStateFlow(true)
     override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
+
     fun setDeviceProvisioned(isProvisioned: Boolean) {
         _isDeviceProvisioned.value = isProvisioned
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return _isDeviceProvisioned.value
+    }
 }
 
 @Module
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
new file mode 100644
index 0000000..db95fad
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
@@ -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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail01_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail01_Test() {
+        mThrown.expectMessage("Method setUp() doesn't have @Before");
+    }
+
+    @SuppressWarnings("JUnit4SetUpNotRun")
+    public void setUp() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
new file mode 100644
index 0000000..ddc66c7
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
@@ -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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail02_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail02_Test() {
+        mThrown.expectMessage("Method tearDown() doesn't have @After");
+    }
+
+    @SuppressWarnings("JUnit4TearDownNotRun")
+    public void tearDown() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
new file mode 100644
index 0000000..ec8e907
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
@@ -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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_Fail03_Test {
+    private ExpectedException mThrown = ExpectedException.none();
+    private final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Rule
+    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
+
+    public RavenwoodTestMethodValidation_Fail03_Test() {
+        mThrown.expectMessage("Method testFoo() doesn't have @Test");
+    }
+
+    @SuppressWarnings("JUnit4TestNotRun")
+    public void testFoo() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
new file mode 100644
index 0000000..d952d07
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
@@ -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.ravenwoodtest.coretest.methodvalidation;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
+ * This class contains tests for this validator.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodTestMethodValidation_OkTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Before
+    public void setUp() {
+    }
+
+    @Before
+    public void testSetUp() {
+    }
+
+    @After
+    public void tearDown() {
+    }
+
+    @After
+    public void testTearDown() {
+    }
+
+    @Test
+    public void testEmpty() {
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 49e793f..4357f2b 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -33,14 +33,17 @@
 import com.android.server.LocalServices;
 
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.Description;
 import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
 import java.io.PrintStream;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -230,6 +233,18 @@
         }
     }
 
+    /**
+     * @return if a method has any of annotations.
+     */
+    private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) {
+        for (var anno : annotations) {
+            if (m.getAnnotation(anno) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static void validateTestAnnotations(Statement base, Description description,
             boolean enableOptionalValidation) {
         final var testClass = description.getTestClass();
@@ -239,13 +254,14 @@
         boolean hasErrors = false;
         for (Method m : collectMethods(testClass)) {
             if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
-                if (m.getAnnotation(Test.class) == null) {
+                if (!hasAnyAnnotations(m, Test.class, Before.class, After.class,
+                        BeforeClass.class, AfterClass.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @Test");
                     hasErrors = true;
                 }
             }
             if ("setUp".equals(m.getName())) {
-                if (m.getAnnotation(Before.class) == null) {
+                if (!hasAnyAnnotations(m, Before.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @Before");
                     hasErrors = true;
                 }
@@ -255,7 +271,7 @@
                 }
             }
             if ("tearDown".equals(m.getName())) {
-                if (m.getAnnotation(After.class) == null) {
+                if (!hasAnyAnnotations(m, After.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @After");
                     hasErrors = true;
                 }
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b4efae3..8e2e0ad 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -114,6 +114,13 @@
 }
 
 flag {
+    name: "enable_magnification_follows_mouse"
+    namespace: "accessibility"
+    description: "Whether to enable mouse following for fullscreen magnification"
+    bug: "335494097"
+}
+
+flag {
     name: "fix_drag_pointer_when_ending_drag"
     namespace: "accessibility"
     description: "Send the correct pointer id when transitioning from dragging to delegating states."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index b918d80..30c743e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -161,6 +161,7 @@
 
 import com.android.internal.R;
 import com.android.internal.accessibility.AccessibilityShortcutController;
+import com.android.internal.accessibility.AccessibilityShortcutController.ExtraDimFrameworkFeatureInfo;
 import com.android.internal.accessibility.AccessibilityShortcutController.FrameworkFeatureInfo;
 import com.android.internal.accessibility.AccessibilityShortcutController.LaunchableFrameworkFeatureInfo;
 import com.android.internal.accessibility.common.ShortcutConstants;
@@ -3910,6 +3911,7 @@
             Slog.d(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName);
             return;
         }
+
         // In case user assigned an accessibility framework feature to the given shortcut.
         if (performAccessibilityFrameworkFeature(displayId, targetComponentName, shortcutType)) {
             return;
@@ -3933,6 +3935,10 @@
         if (!frameworkFeatureMap.containsKey(assignedTarget)) {
             return false;
         }
+        final int userId;
+        synchronized (mLock) {
+            userId = mCurrentUserId;
+        }
         final FrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(assignedTarget);
         final SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(),
                 featureInfo.getSettingKey(), mCurrentUserId);
@@ -3944,6 +3950,15 @@
             return true;
         }
 
+        if (featureInfo instanceof ExtraDimFrameworkFeatureInfo) {
+            boolean serviceEnabled =
+                    ((ExtraDimFrameworkFeatureInfo) featureInfo)
+                            .activateShortcut(mContext, userId);
+            logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
+                    serviceEnabled);
+            return true;
+        }
+
         // Assuming that the default state will be to have the feature off
         if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) {
             logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index ad8f5e1..668852b 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -31,12 +31,14 @@
 import android.metrics.LogMaker;
 import android.os.UserManager;
 import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
 import android.service.autofill.InternalSanitizer;
 import android.service.autofill.SaveInfo;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.View;
 import android.view.WindowManager;
@@ -374,4 +376,50 @@
     private interface ViewNodeFilter {
         boolean matches(ViewNode node);
     }
+
+    public static class SaveInfoStats {
+        public int saveInfoCount;
+        public int saveDataTypeCount;
+
+        public SaveInfoStats(int saveInfoCount, int saveDataTypeCount) {
+            this.saveInfoCount = saveInfoCount;
+            this.saveDataTypeCount = saveDataTypeCount;
+        }
+    }
+
+    /**
+     * Get statistic information of save info given a sparse array of fill responses.
+     *
+     * Specifically the statistic includes
+     *   1. how many save info the current session has.
+     *   2. How many distinct save data types current session has.
+     *
+     * @return SaveInfoStats returns the above two number in a SaveInfoStats object
+     */
+    public static SaveInfoStats getSaveInfoStatsFromFillResponses(
+            SparseArray<FillResponse> fillResponses) {
+        if (fillResponses == null) {
+            if (sVerbose) {
+                Slog.v(TAG, "getSaveInfoStatsFromFillResponses(): fillResponse sparse array is "
+                        + "null");
+            }
+            return new SaveInfoStats(-1, -1);
+        }
+        int numSaveInfos = 0;
+        int numSaveDataTypes = 0;
+        ArraySet<Integer> saveDataTypeSeen = new ArraySet<>();
+        final int numResponses = fillResponses.size();
+        for (int responseNum = 0; responseNum < numResponses; responseNum++) {
+            final FillResponse response = fillResponses.valueAt(responseNum);
+            if (response != null && response.getSaveInfo() != null) {
+                numSaveInfos += 1;
+                int saveDataType = response.getSaveInfo().getType();
+                if (!saveDataTypeSeen.contains(saveDataType)) {
+                    saveDataTypeSeen.add(saveDataType);
+                    numSaveDataTypes += 1;
+                }
+            }
+        }
+        return new SaveInfoStats(numSaveInfos, numSaveDataTypes);
+    }
 }
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index d7da2f0..49ca297 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -804,7 +804,15 @@
                     + event.mSuggestionPresentedLastTimestampMs
                     + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId
                     + " event.mFieldFirstLength=" + event.mFieldFirstLength
-                    + " event.mFieldLastLength=" + event.mFieldLastLength);
+                    + " event.mFieldLastLength=" + event.mFieldLastLength
+                    + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount
+                    + " event.mViewFilledSuccessfullyOnRefillCount="
+                    + event.mViewFilledSuccessfullyOnRefillCount
+                    + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount
+                    + " event.notExpiringResponseDuringAuthCount="
+                    + event.mFixExpireResponseDuringAuthCount
+                    + " event.notifyViewEnteredIgnoredDuringAuthCount="
+                    + event.mNotifyViewEnteredIgnoredDuringAuthCount);
         }
 
         // TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -846,7 +854,6 @@
                 mCallingAppUid,
                 event.mIsCredentialRequest,
                 event.mWebviewRequestedCredential,
-                event.mFilteredFillabaleViewCount,
                 event.mViewFillableTotalCount,
                 event.mViewFillFailureCount,
                 event.mFocusedId,
@@ -859,7 +866,13 @@
                 event.mSuggestionPresentedLastTimestampMs,
                 event.mFocusedVirtualAutofillId,
                 event.mFieldFirstLength,
-                event.mFieldLastLength);
+                event.mFieldLastLength,
+                event.mFilteredFillabaleViewCount,
+                event.mViewFailedPriorToRefillCount,
+                event.mViewFilledSuccessfullyOnRefillCount,
+                event.mViewFailedOnRefillCount,
+                event.mFixExpireResponseDuringAuthCount,
+                event.mNotifyViewEnteredIgnoredDuringAuthCount);
         mEventInternal = Optional.empty();
     }
 
@@ -912,6 +925,12 @@
         // uninitialized doesn't help much, as this would be non-zero only if callback is received.
         int mViewFillSuccessCount = 0;
         int mViewFilledButUnexpectedCount = 0;
+        int mViewFailedPriorToRefillCount = 0;
+        int mViewFailedOnRefillCount = 0;
+        int mViewFilledSuccessfullyOnRefillCount = 0;
+
+        int mFixExpireResponseDuringAuthCount = 0;
+        int mNotifyViewEnteredIgnoredDuringAuthCount = 0;
 
         ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
         ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>();
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index c6ddc16..21df7a5 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -70,6 +70,7 @@
 import static com.android.server.autofill.Helper.containsCharsInOrder;
 import static com.android.server.autofill.Helper.createSanitizers;
 import static com.android.server.autofill.Helper.getNumericValue;
+import static com.android.server.autofill.Helper.SaveInfoStats;
 import static com.android.server.autofill.Helper.sDebug;
 import static com.android.server.autofill.Helper.sVerbose;
 import static com.android.server.autofill.Helper.toArray;
@@ -3203,11 +3204,6 @@
         return saveInfo == null ? 0 : saveInfo.getFlags();
     }
 
-    static class SaveInfoStats {
-        public int saveInfoCount;
-        public int saveDataTypeCount;
-    }
-
     /**
      * Get statistic information of save info in current session. Specifically
      *   1. how many save info the current session has.
@@ -3217,42 +3213,13 @@
      */
     @GuardedBy("mLock")
     private SaveInfoStats getSaveInfoStatsLocked() {
-        SaveInfoStats retSaveInfoStats = new SaveInfoStats();
-        retSaveInfoStats.saveInfoCount = -1;
-        retSaveInfoStats.saveDataTypeCount = -1;
-
         if (mContexts == null) {
             if (sVerbose) {
                 Slog.v(TAG, "getSaveInfoStatsLocked(): mContexts is null");
             }
-        } else if (mResponses == null) {
-            // Happens when the activity / session was finished before the service replied, or
-            // when the service cannot autofill it (and returned a null response).
-            if (sVerbose) {
-                Slog.v(TAG, "getSaveInfoStatsLocked(): mResponses is null");
-            }
-            return retSaveInfoStats;
-        } else {
-            int numSaveInfos = 0;
-            int numSaveDataTypes = 0;
-            ArraySet<Integer> saveDataTypeSeen = new ArraySet<>();
-            final int numResponses = mResponses.size();
-            for (int responseNum = 0; responseNum < numResponses; responseNum++) {
-                final FillResponse response = mResponses.valueAt(responseNum);
-                if (response != null && response.getSaveInfo() != null) {
-                    numSaveInfos += 1;
-                    int saveDataType = response.getSaveInfo().getType();
-                    if (!saveDataTypeSeen.contains(saveDataType)) {
-                        saveDataTypeSeen.add(saveDataType);
-                        numSaveDataTypes += 1;
-                    }
-                }
-            }
-            retSaveInfoStats.saveInfoCount = numSaveInfos;
-            retSaveInfoStats.saveDataTypeCount = numSaveDataTypes;
+            return new SaveInfoStats(-1, -1);
         }
-
-        return retSaveInfoStats;
+        return Helper.getSaveInfoStatsFromFillResponses(mResponses);
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 7d9d660..ee7d0ae 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -28,9 +28,9 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
-import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
 import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix;
+import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -561,8 +561,8 @@
     private void sendPendingIntent(int displayId, PendingIntent pendingIntent)
             throws PendingIntent.CanceledException {
         final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
-        options.setPendingIntentBackgroundActivityLaunchAllowed(true);
-        options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
         pendingIntent.send(
                 mContext,
                 /* code= */ 0,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 69ee8fc..cf0befa 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16961,16 +16961,24 @@
 
         int userId = UserHandle.getCallingUserId();
 
-        if (UserManager.isVisibleBackgroundUsersEnabled() && userId != getCurrentUserId()) {
-            // The check is added mainly for auto devices. On auto devices, it is possible that
-            // multiple users are visible simultaneously using visible background users.
-            // In such cases, it is desired that only the current user (not the visible background
-            // user) can change the locale and other persistent settings of the device.
-            Slog.w(TAG, "Only current user is allowed to update persistent configuration if "
-                    + "visible background users are enabled. Current User" + getCurrentUserId()
-                    + ". Calling User: " + userId);
-            throw new SecurityException("Only current user is allowed to update persistent "
-                    + "configuration.");
+        if (UserManager.isVisibleBackgroundUsersEnabled()) {
+            final long origId = Binder.clearCallingIdentity();
+            try {
+                if (userId != getCurrentUserId()) {
+                    // The check is added mainly for auto devices. On auto devices, it is
+                    // possible that multiple users are visible simultaneously using visible
+                    // background users. In such cases, it is desired that only the current user
+                    // (not the visible background user) can change the locale and other persistent
+                    // settings of the device.
+                    Slog.w(TAG, "Only current user is allowed to update persistent configuration "
+                            + "if visible background users are enabled. Current User"
+                            + getCurrentUserId() + ". Calling User: " + userId);
+                    throw new SecurityException("Only current user is allowed to update persistent "
+                            + "configuration.");
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
         }
 
         mActivityTaskManager.updatePersistentConfiguration(values, userId);
diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java
index 061bcd7..ee7033e 100644
--- a/services/core/java/com/android/server/am/AppErrors.java
+++ b/services/core/java/com/android/server/am/AppErrors.java
@@ -47,6 +47,7 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -63,6 +64,8 @@
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.server.LocalServices;
 import com.android.server.PackageWatchdog;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.UserManagerService;
 import com.android.server.usage.AppStandbyInternal;
 import com.android.server.wm.WindowProcessController;
 
@@ -868,9 +871,6 @@
     private boolean handleAppCrashLSPB(ProcessRecord app, String reason,
             String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) {
         final long now = SystemClock.uptimeMillis();
-        final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                Settings.Secure.ANR_SHOW_BACKGROUND, 0,
-                mService.mUserController.getCurrentUserId()) != 0;
 
         Long crashTime;
         Long crashTimePersistent;
@@ -881,6 +881,8 @@
         final boolean persistent = app.isPersistent();
         final WindowProcessController proc = app.getWindowProcessController();
         final ProcessErrorStateRecord errState = app.mErrorState;
+        final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.ANR_SHOW_BACKGROUND, 0, getVisibleUserId(userId)) != 0;
 
         if (!app.isolated) {
             crashTime = mProcessCrashTimes.get(processName, uid);
@@ -1000,9 +1002,6 @@
 
     void handleShowAppErrorUi(Message msg) {
         AppErrorDialog.Data data = (AppErrorDialog.Data) msg.obj;
-        boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                Settings.Secure.ANR_SHOW_BACKGROUND, 0,
-                mService.mUserController.getCurrentUserId()) != 0;
 
         final int userId;
         synchronized (mProcLock) {
@@ -1027,7 +1026,11 @@
             for (int profileId : mService.mUserController.getCurrentProfileIds()) {
                 isBackground &= (userId != profileId);
             }
-            if (isBackground && !showBackground) {
+            int visibleUserId = getVisibleUserId(userId);
+            boolean isVisibleUser = isVisibleBackgroundUser(visibleUserId);
+            boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                    Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0;
+            if (isBackground && !showBackground && !isVisibleUser) {
                 Slog.w(TAG, "Skipping crash dialog of " + proc + ": background");
                 if (res != null) {
                     res.set(AppErrorDialog.BACKGROUND_USER);
@@ -1054,7 +1057,7 @@
                 final long now = SystemClock.uptimeMillis();
                 final boolean shouldThottle = crashShowErrorTime != null
                         && now < crashShowErrorTime + ActivityManagerConstants.MIN_CRASH_INTERVAL;
-                if ((mService.mAtmInternal.canShowErrorDialogs() || showBackground)
+                if ((mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground)
                         && !crashSilenced && !shouldThottle
                         && (showFirstCrash || showFirstCrashDevOption || data.repeating)) {
                     Slog.i(TAG, "Showing crash dialog for package " + packageName + " u" + userId);
@@ -1103,10 +1106,10 @@
                 return;
             }
 
+            int visibleUserId = getVisibleUserId(proc.userId);
             boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                    Settings.Secure.ANR_SHOW_BACKGROUND, 0,
-                    mService.mUserController.getCurrentUserId()) != 0;
-            if (mService.mAtmInternal.canShowErrorDialogs() || showBackground) {
+                    Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0;
+            if (mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground) {
                 AnrController anrController = errState.getDialogController().getAnrController();
                 if (anrController == null) {
                     errState.getDialogController().showAnrDialogs(data);
@@ -1163,6 +1166,43 @@
     }
 
     /**
+     * Returns the user ID of the visible user associated with the error occurrence.
+     *
+     * <p>For most cases it will return the current foreground user ID, but on devices that
+     * {@link UserManager#isVisibleBackgroundUsersEnabled() support visible background users},
+     * it will return the given app user ID passed as parameter.
+     *
+     * @param appUserId The user ID of the app where the error occurred.
+     * @return The ID of the visible user associated with the error.
+     */
+    private int getVisibleUserId(int appUserId) {
+        if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+            return mService.mUserController.getCurrentUserId();
+        }
+        return appUserId;
+    }
+
+    /**
+     * Checks if the given user is a visible background user, which is a full, background user
+     * assigned to secondary displays on the devices that have
+     * {@link UserManager#isVisibleBackgroundUsersEnabled()
+     * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on
+     * automotive builds, using the display associated with their seats).
+     *
+     * @see UserManager#isUserVisible()
+     */
+    private boolean isVisibleBackgroundUser(int userId) {
+        if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+            return false;
+        }
+        boolean isForeground = mService.mUserController.getCurrentUserId() == userId;
+        boolean isProfile = UserManagerService.getInstance().isProfile(userId);
+        boolean isVisible = LocalServices.getService(UserManagerInternal.class)
+                .isUserVisible(userId);
+        return isVisible && !isForeground && !isProfile;
+    }
+
+    /**
      * Information about a process that is currently marked as bad.
      */
     static final class BadProcessInfo {
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 092ee16..8df4e77 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -672,6 +672,12 @@
                 BatteryConsumer.POWER_COMPONENT_FLASHLIGHT,
                 Flags.streamlinedMiscBatteryStats());
 
+        mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_GNSS,
+                Flags.streamlinedMiscBatteryStats());
+        mBatteryUsageStatsProvider.setPowerStatsExporterEnabled(
+                BatteryConsumer.POWER_COMPONENT_GNSS,
+                Flags.streamlinedMiscBatteryStats());
+
         mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CAMERA,
                 Flags.streamlinedMiscBatteryStats());
         mBatteryUsageStatsProvider.setPowerStatsExporterEnabled(
@@ -1098,18 +1104,20 @@
         final StatsManager statsManager = mContext.getSystemService(StatsManager.class);
         final StatsPullAtomCallbackImpl pullAtomCallback = new StatsPullAtomCallbackImpl();
 
-        statsManager.setPullAtomCallback(
-                FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET,
-                null, // use default PullAtomMetadata values
-                DIRECT_EXECUTOR, pullAtomCallback);
-        statsManager.setPullAtomCallback(
-                FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL,
-                null, // use default PullAtomMetadata values
-                DIRECT_EXECUTOR, pullAtomCallback);
-        statsManager.setPullAtomCallback(
-                FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET,
-                null, // use default PullAtomMetadata values
-                DIRECT_EXECUTOR, pullAtomCallback);
+        if (!Flags.disableCompositeBatteryUsageStatsAtoms()) {
+            statsManager.setPullAtomCallback(
+                    FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET,
+                    null, // use default PullAtomMetadata values
+                    DIRECT_EXECUTOR, pullAtomCallback);
+            statsManager.setPullAtomCallback(
+                    FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL,
+                    null, // use default PullAtomMetadata values
+                    DIRECT_EXECUTOR, pullAtomCallback);
+            statsManager.setPullAtomCallback(
+                    FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET,
+                    null, // use default PullAtomMetadata values
+                    DIRECT_EXECUTOR, pullAtomCallback);
+        }
         if (Flags.addBatteryUsageStatsSliceAtom()) {
             statsManager.setPullAtomCallback(
                     FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID,
@@ -1126,6 +1134,10 @@
             final BatteryUsageStats bus;
             switch (atomTag) {
                 case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET: {
+                    if (Flags.disableCompositeBatteryUsageStatsAtoms()) {
+                        return StatsManager.PULL_SKIP;
+                    }
+
                     @SuppressLint("MissingPermission")
                     final double minConsumedPowerThreshold =
                             DeviceConfig.getFloat(DEVICE_CONFIG_NAMESPACE,
@@ -1142,6 +1154,10 @@
                     break;
                 }
                 case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL:
+                    if (Flags.disableCompositeBatteryUsageStatsAtoms()) {
+                        return StatsManager.PULL_SKIP;
+                    }
+
                     final BatteryUsageStatsQuery queryPowerProfile =
                             new BatteryUsageStatsQuery.Builder()
                                     .setMaxStatsAgeMs(0)
@@ -1153,6 +1169,10 @@
                     bus = getBatteryUsageStats(List.of(queryPowerProfile)).get(0);
                     break;
                 case FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET: {
+                    if (Flags.disableCompositeBatteryUsageStatsAtoms()) {
+                        return StatsManager.PULL_SKIP;
+                    }
+
                     final long sessionStart =
                             getLastBatteryUsageStatsBeforeResetAtomPullTimestamp();
                     final long sessionEnd;
@@ -3252,6 +3272,9 @@
                 .setMaxStatsAgeMs(0)
                 .includeProcessStateData()
                 .includePowerModels();
+        if (Flags.batteryUsageStatsByPowerAndScreenState()) {
+            builder.includeScreenStateData().includePowerStateData();
+        }
         if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
             builder.powerProfileModeledOnly();
         }
@@ -3270,7 +3293,7 @@
         if (proto) {
             batteryUsageStats.dumpToProto(fd);
         } else {
-            batteryUsageStats.dump(pw, "");
+            batteryUsageStats.dump(pw, "  ");
         }
     }
 
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b058bd8..504c54a 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1708,6 +1708,11 @@
                 // priority for this non-top split.
                 schedGroup = SCHED_GROUP_TOP_APP;
                 mAdjType = "resumed-split-screen-activity";
+            } else if ((flags
+                    & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) {
+                // The recently used non-top visible freeform app.
+                schedGroup = SCHED_GROUP_TOP_APP;
+                mAdjType = "perceptible-freeform-activity";
             }
             foregroundActivities = true;
             mHasVisibleActivities = true;
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index f6df60f..e4c65bd2 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -269,7 +269,8 @@
     @Override
     public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
             String[] args, ShellCallback callback, ResultReceiver result) {
-        new GameManagerShellCommand().exec(this, in, out, err, args, callback, result);
+        new GameManagerShellCommand(mPackageManager).exec(this, in, out, err, args, callback,
+                result);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/app/GameManagerShellCommand.java b/services/core/java/com/android/server/app/GameManagerShellCommand.java
index ab57c4f..d3b4312 100644
--- a/services/core/java/com/android/server/app/GameManagerShellCommand.java
+++ b/services/core/java/com/android/server/app/GameManagerShellCommand.java
@@ -16,10 +16,13 @@
 
 package com.android.server.app;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.GameManager;
 import android.app.IGameManagerService;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -47,7 +50,10 @@
     private static final String UNSUPPORTED_MODE_NUM = String.valueOf(
             GameManager.GAME_MODE_UNSUPPORTED);
 
-    public GameManagerShellCommand() {
+    private PackageManager mPackageManager;
+
+    public GameManagerShellCommand(@NonNull PackageManager packageManager) {
+        mPackageManager = packageManager;
     }
 
     @Override
@@ -91,9 +97,29 @@
         return -1;
     }
 
+    private boolean isPackageGame(String packageName, int userId, PrintWriter pw) {
+        try {
+            final ApplicationInfo applicationInfo = mPackageManager
+                    .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId);
+            boolean isGame = applicationInfo.category == ApplicationInfo.CATEGORY_GAME;
+            if (!isGame) {
+                pw.println("Package " + packageName + " is not of game type, to use the game "
+                        + "mode commands, it must specify game category in the manifest as "
+                        + "android:appCategory=\"game\"");
+            }
+            return isGame;
+        } catch (PackageManager.NameNotFoundException e) {
+            pw.println("Package " + packageName + " is not found for user " + userId);
+            return false;
+        }
+    }
+
     private int runListGameModes(PrintWriter pw) throws ServiceNotFoundException, RemoteException {
         final String packageName = getNextArgRequired();
         final int userId = ActivityManager.getCurrentUser();
+        if (!isPackageGame(packageName, userId, pw)) {
+            return -1;
+        }
         final GameManagerService gameManagerService = (GameManagerService)
                 ServiceManager.getService(Context.GAME_SERVICE);
         final String currentMode = gameModeIntToString(
@@ -110,12 +136,15 @@
     private int runListGameModeConfigs(PrintWriter pw)
             throws ServiceNotFoundException, RemoteException {
         final String packageName = getNextArgRequired();
-
+        final int userId = ActivityManager.getCurrentUser();
+        if (!isPackageGame(packageName, userId, pw)) {
+            return -1;
+        }
         final GameManagerService gameManagerService = (GameManagerService)
                 ServiceManager.getService(Context.GAME_SERVICE);
 
         final String listStr = gameManagerService.getInterventionList(packageName,
-                ActivityManager.getCurrentUser());
+                userId);
 
         if (listStr == null) {
             pw.println("No interventions found for " + packageName);
@@ -131,15 +160,17 @@
         if (option != null && option.equals("--user")) {
             userIdStr = getNextArgRequired();
         }
-
         final String gameMode = getNextArgRequired();
         final String packageName = getNextArgRequired();
+        int userId = userIdStr != null ? Integer.parseInt(userIdStr)
+                : ActivityManager.getCurrentUser();
+        if (!isPackageGame(packageName, userId, pw)) {
+            return -1;
+        }
         final IGameManagerService service = IGameManagerService.Stub.asInterface(
                 ServiceManager.getServiceOrThrow(Context.GAME_SERVICE));
         boolean batteryModeSupported = false;
         boolean perfModeSupported = false;
-        int userId = userIdStr != null ? Integer.parseInt(userIdStr)
-                : ActivityManager.getCurrentUser();
         int[] modes = service.getAvailableGameModes(packageName, userId);
         for (int mode : modes) {
             if (mode == GameManager.GAME_MODE_PERFORMANCE) {
@@ -262,6 +293,9 @@
 
         int userId = userIdStr != null ? Integer.parseInt(userIdStr)
                 : ActivityManager.getCurrentUser();
+        if (!isPackageGame(packageName, userId, pw)) {
+            return -1;
+        }
 
         final GameManagerService gameManagerService = (GameManagerService)
                 ServiceManager.getService(Context.GAME_SERVICE);
@@ -308,13 +342,14 @@
         }
 
         final String packageName = getNextArgRequired();
+        int userId = userIdStr != null ? Integer.parseInt(userIdStr)
+                : ActivityManager.getCurrentUser();
+        if (!isPackageGame(packageName, userId, pw)) {
+            return -1;
+        }
 
         final GameManagerService gameManagerService = (GameManagerService)
                 ServiceManager.getService(Context.GAME_SERVICE);
-
-        int userId = userIdStr != null ? Integer.parseInt(userIdStr)
-                : ActivityManager.getCurrentUser();
-
         if (gameMode == null) {
             gameManagerService.resetGameModeConfigOverride(packageName, userId, -1);
             return 0;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 326ed69..25b9228 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1871,7 +1871,10 @@
                             synchronized (mBluetoothAudioStateLock) {
                                 reapplyAudioHalBluetoothState();
                             }
-                            mBtHelper.onAudioServerDiedRestoreA2dp();
+                            final int forceForMedia = getBluetoothA2dpEnabled()
+                                    ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
+                            setForceUse_Async(
+                                    AudioSystem.FOR_MEDIA, forceForMedia, "MSG_RESTORE_DEVICES");
                             updateCommunicationRoute("MSG_RESTORE_DEVICES");
                         }
                     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 02aa6f5..ca69f31 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -1757,6 +1757,15 @@
             if (AudioService.DEBUG_DEVICES) {
                 Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected);
             }
+            // Do not report an error in case of redundant connect or disconnect request
+            // as this can cause a state mismatch between BtHelper and AudioDeviceInventory
+            if (connect == isConnected) {
+                Log.i(TAG, "handleDeviceConnection() deviceInfo=" + di + " is already "
+                        + (connect ? "" : "dis") + "connected");
+                mmi.set(MediaMetrics.Property.STATE, connect
+                        ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT).record();
+                return true;
+            }
             if (connect && !isConnected) {
                 final int res;
                 if (isForTesting) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index fab0a56..fe3bbb0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -739,6 +739,8 @@
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
+    private final Executor mAudioServerLifecycleExecutor;
+
     private IMediaProjectionManager mProjectionService; // to validate projection token
 
     /** Interface for UserManagerService. */
@@ -1059,7 +1061,8 @@
                               audioserverPermissions() ?
                                 initializeAudioServerPermissionProvider(
                                     context, audioPolicyFacade, audioserverLifecycleExecutor) :
-                                    null
+                                    null,
+                              audioserverLifecycleExecutor
                               );
         }
 
@@ -1145,13 +1148,16 @@
      *               {@link AudioSystemThread} is created as the messaging thread instead.
      * @param appOps {@link AppOpsManager} system service
      * @param enforcer Used for permission enforcing
+     * @param permissionProvider Used to push permissions to audioserver
+     * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle
      */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings,
             AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
             @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer,
-            /* @NonNull */ AudioServerPermissionProvider permissionProvider) {
+            /* @NonNull */ AudioServerPermissionProvider permissionProvider,
+            Executor audioserverLifecycleExecutor) {
         super(enforcer);
         sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
@@ -1159,6 +1165,7 @@
         mAppOps = appOps;
 
         mPermissionProvider = permissionProvider;
+        mAudioServerLifecycleExecutor = audioserverLifecycleExecutor;
 
         mAudioSystem = audioSystem;
         mSystemServer = systemServer;
@@ -1170,6 +1177,34 @@
         mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast");
         mBroadcastHandlerThread.start();
 
+        // Listen to permission invalidations for the PermissionProvider
+        if (audioserverPermissions()) {
+            final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler();
+            mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO,
+                    new Runnable() {
+                        // Roughly chosen to be long enough to suppress the autocork behavior
+                        // of the permission cache (50ms), and longer than the task could reasonably
+                        // take, even with many packages and users, while not introducing visible
+                        // permission leaks - since the app needs to restart, and trigger an action
+                        // which requires permissions from audioserver before this delay.
+                        // For RECORD_AUDIO, we are additionally protected by appops.
+                        final long UPDATE_DELAY_MS = 110;
+                        final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0);
+                        @Override
+                        public void run() {
+                            var currentTime = SystemClock.uptimeMillis();
+                            if (currentTime > scheduledUpdateTimestamp.get()) {
+                                scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS);
+                                broadcastHandler.postAtTime( () ->
+                                        mAudioServerLifecycleExecutor.execute(mPermissionProvider
+                                            ::onPermissionStateChanged),
+                                        currentTime + UPDATE_DELAY_MS
+                                    );
+                            }
+                        }
+            });
+        }
+
         mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem);
 
         mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -9918,9 +9953,9 @@
             int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
             synchronized (mCachedAbsVolDrivingStreamsLock) {
                 mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> {
-                    if (stream != null && !mAvrcpAbsVolSupported) {
+                    if (!mAvrcpAbsVolSupported) {
                         mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
-                                "", /*enabled*/false, AudioSystem.DEVICE_NONE);
+                                "", /*enabled*/false, AudioSystem.STREAM_DEFAULT);
                         return null;
                     }
                     // For A2DP and AVRCP we need to set the driving stream based on the
@@ -11974,29 +12009,6 @@
             provider.onServiceStart(audioPolicy.getPermissionController());
         });
 
-        // Set up event listeners
-        // Must be kept in sync with PermissionManager
-        Runnable cacheSysPropHandler = new Runnable() {
-            private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference();
-            private AtomicLong mNonce = new AtomicLong();
-            @Override
-            public void run() {
-                if (mHandle.get() == null) {
-                    // Cache the handle
-                    mHandle.compareAndSet(null, SystemProperties.find(
-                            PermissionManager.CACHE_KEY_PACKAGE_INFO));
-                }
-                long nonce;
-                SystemProperties.Handle ref;
-                if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 &&
-                        mNonce.getAndSet(nonce) != nonce) {
-                    audioserverExecutor.execute(() -> provider.onPermissionStateChanged());
-                }
-            }
-        };
-
-        SystemProperties.addChangeCallback(cacheSysPropHandler);
-
         IntentFilter packageUpdateFilter = new IntentFilter();
         packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED);
         packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED);
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7f4bc74..d083c68 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -748,6 +748,10 @@
         return AudioSystem.setMasterMute(mute);
     }
 
+    public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) {
+        AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback);
+    }
+
     /**
      * Part of AudioService dump
      * @param pw
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 57bffa7..ce92dfb 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -83,39 +83,53 @@
     }
 
     // BluetoothHeadset API to control SCO connection
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothHeadset mBluetoothHeadset;
 
     // Bluetooth headset device
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private @Nullable BluetoothDevice mBluetoothHeadsetDevice;
+
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private final Map<BluetoothDevice, AudioDeviceAttributes> mResolvedScoAudioDevices =
             new HashMap<>();
 
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothHearingAid mHearingAid = null;
 
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothLeAudio mLeAudio = null;
 
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothLeAudioCodecConfig mLeAudioCodecConfig;
 
     // Reference to BluetoothA2dp to query for AbsoluteVolume.
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothA2dp mA2dp = null;
 
+    @GuardedBy("BtHelper.this")
     private @Nullable BluetoothCodecConfig mA2dpCodecConfig;
 
+    @GuardedBy("BtHelper.this")
     private @AudioSystem.AudioFormatNativeEnumForBtCodec
             int mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT;
 
     // If absolute volume is supported in AVRCP device
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private boolean mAvrcpAbsVolSupported = false;
 
     // Current connection state indicated by bluetooth headset
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private int mScoConnectionState;
 
     // Indicate if SCO audio connection is currently active and if the initiator is
     // audio service (internal) or bluetooth headset (external)
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private int mScoAudioState;
 
     // Indicates the mode used for SCO audio connection. The mode is virtual call if the request
     // originated from an app targeting an API version before JB MR2 and raw audio after that.
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private int mScoAudioMode;
 
     // SCO audio state is not active
@@ -210,7 +224,7 @@
     //----------------------------------------------------------------------
     // Interface for AudioDeviceBroker
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onSystemReady() {
         mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
         resetBluetoothSco();
@@ -238,17 +252,13 @@
         }
     }
 
-    /*package*/ synchronized void onAudioServerDiedRestoreA2dp() {
-        final int forMed = mDeviceBroker.getBluetoothA2dpEnabled()
-                ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
-        mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()");
-    }
-
-    /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean supported) {
         mAvrcpAbsVolSupported = supported;
         Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
     }
 
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
         if (mA2dp == null) {
             if (AudioService.DEBUG_VOL) {
@@ -371,7 +381,7 @@
         return codecAndChanged;
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onReceiveBtEvent(Intent intent) {
         final String action = intent.getAction();
 
@@ -393,12 +403,12 @@
     }
 
     /**
-     * Exclusively called from AudioDeviceBroker (with mSetModeLock held)
+     * Exclusively called from AudioDeviceBroker (with mDeviceStateLock held)
      * when handling MSG_L_RECEIVED_BT_EVENT in {@link #onReceiveBtEvent(Intent)}
      * as part of the serialization of the communication route selection
      */
-    @GuardedBy("BtHelper.this")
-    private void onScoAudioStateChanged(int state) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    private synchronized void onScoAudioStateChanged(int state) {
         boolean broadcast = false;
         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
         Log.i(TAG, "onScoAudioStateChanged  state: " + state
@@ -414,12 +424,14 @@
                     broadcast = true;
                 }
                 if (!mDeviceBroker.isScoManagedByAudio()) {
-                    mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
+                    mDeviceBroker.setBluetoothScoOn(
+                            true, "BtHelper.onScoAudioStateChanged, state: " + state);
                 }
                 break;
             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
                 if (!mDeviceBroker.isScoManagedByAudio()) {
-                    mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
+                    mDeviceBroker.setBluetoothScoOn(
+                            false, "BtHelper.onScoAudioStateChanged, state: " + state);
                 }
                 scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
                 // There are two cases where we want to immediately reconnect audio:
@@ -466,6 +478,7 @@
      *
      * @return false if SCO isn't connected
      */
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized boolean isBluetoothScoOn() {
         if (mBluetoothHeadset == null || mBluetoothHeadsetDevice == null) {
             return false;
@@ -479,19 +492,20 @@
         return false;
     }
 
-    /*package*/ synchronized boolean isBluetoothScoRequestedInternally() {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    /*package*/ boolean isBluetoothScoRequestedInternally() {
         return mScoAudioState == SCO_STATE_ACTIVE_INTERNAL
               || mScoAudioState == SCO_STATE_ACTIVATE_REQ;
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
                 @NonNull String eventSource) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL);
@@ -551,7 +565,8 @@
         }
     }
 
-    /*package*/ synchronized void onBroadcastScoConnectionState(int state) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    /*package*/ void onBroadcastScoConnectionState(int state) {
         if (state == mScoConnectionState) {
             return;
         }
@@ -563,8 +578,8 @@
         mScoConnectionState = state;
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
-    /*package*/ synchronized void resetBluetoothSco() {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    /*package*/ void resetBluetoothSco() {
         mScoAudioState = SCO_STATE_INACTIVE;
         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
         mDeviceBroker.clearA2dpSuspended(false /* internalOnly */);
@@ -572,7 +587,7 @@
         mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onBtProfileDisconnected(int profile) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "BT profile " + BluetoothProfile.getProfileName(profile)
@@ -634,9 +649,10 @@
         }
     }
 
+    @GuardedBy("BtHelper.this")
     MyLeAudioCallback mLeAudioCallback = null;
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
         AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "BT profile " + BluetoothProfile.getProfileName(profile) + " connected to proxy "
@@ -773,8 +789,8 @@
         }
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
-    private void onHeadsetProfileConnected(@NonNull BluetoothHeadset headset) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    private synchronized void onHeadsetProfileConnected(@NonNull BluetoothHeadset headset) {
         // Discard timeout message
         mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
         mBluetoothHeadset = headset;
@@ -830,6 +846,7 @@
         mDeviceBroker.postBroadcastScoConnectionState(state);
     }
 
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     @Nullable AudioDeviceAttributes getHeadsetAudioDevice() {
         if (mBluetoothHeadsetDevice == null) {
             return null;
@@ -837,6 +854,7 @@
         return getHeadsetAudioDevice(mBluetoothHeadsetDevice);
     }
 
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private @NonNull AudioDeviceAttributes getHeadsetAudioDevice(BluetoothDevice btDevice) {
         AudioDeviceAttributes deviceAttr = mResolvedScoAudioDevices.get(btDevice);
         if (deviceAttr != null) {
@@ -876,30 +894,44 @@
         return new AudioDeviceAttributes(nativeType, address, name);
     }
 
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
     private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
         if (btDevice == null) {
             return true;
         }
-        int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
-        AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice);
         boolean result = false;
+        AudioDeviceAttributes audioDevice = null; // Only used if isActive is true
+        String address = btDevice.getAddress();
+        String name = getName(btDevice);
+        // Handle output device
         if (isActive) {
-            result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice);
+            audioDevice = btHeadsetDeviceToAudioDevice(btDevice);
+            result = mDeviceBroker.handleDeviceConnection(
+                    audioDevice, true /*connect*/, btDevice);
         } else {
-            int[] outDeviceTypes = {
+            AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice);
+            if (ada != null) {
+                result = mDeviceBroker.handleDeviceConnection(
+                    ada, false /*connect*/, btDevice);
+            } else {
+                // Disconnect all possible audio device types if the disconnected device type is
+                // unknown
+                int[] outDeviceTypes = {
                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO,
                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET,
                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT
-            };
-            for (int outDeviceType : outDeviceTypes) {
-                result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                        outDeviceType, audioDevice.getAddress(), audioDevice.getName()),
-                        isActive, btDevice);
+                };
+                for (int outDeviceType : outDeviceTypes) {
+                    result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
+                            outDeviceType, address, name), false /*connect*/, btDevice);
+                }
             }
         }
+        // Handle input device
+        int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
         // handleDeviceConnection() && result to make sure the method get executed
         result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
-                        inDevice, audioDevice.getAddress(), audioDevice.getName()),
+                        inDevice, address, name),
                 isActive, btDevice) && result;
         if (result) {
             if (isActive) {
@@ -916,8 +948,8 @@
         return btDevice == null ? "(null)" : btDevice.getAnonymizedAddress();
     }
 
-    // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock
-    /*package */ synchronized void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) {
         Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice)
                 + " -> " + getAnonymizedAddress(btDevice));
         final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
@@ -987,10 +1019,8 @@
 
     //----------------------------------------------------------------------
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    // @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
-    @GuardedBy("BtHelper.this")
-    private boolean requestScoState(int state, int scoAudioMode) {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    private synchronized boolean requestScoState(int state, int scoAudioMode) {
         checkScoAudioState();
         if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
             // Make sure that the state transitions to CONNECTING even if we cannot initiate
@@ -1154,13 +1184,14 @@
         }
     }
 
-    private void checkScoAudioState() {
+    @GuardedBy("mDeviceBroker.mDeviceStateLock")
+    private synchronized void checkScoAudioState() {
         try {
             if (mBluetoothHeadset != null
                     && mBluetoothHeadsetDevice != null
                     && mScoAudioState == SCO_STATE_INACTIVE
                     && mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
-                    != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
+                        != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
                 mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
             }
         } catch (Exception e) {
@@ -1184,7 +1215,7 @@
         return result;
     }
 
-    /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+    /*package*/ synchronized int getLeAudioDeviceGroupId(BluetoothDevice device) {
         if (mLeAudio == null || device == null) {
             return BluetoothLeAudio.GROUP_ID_INVALID;
         }
@@ -1197,7 +1228,7 @@
      * @return A List of Pair(String main_address, String identity_address). Note that the
      * addresses returned by BluetoothDevice can be null.
      */
-    /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
+    /*package*/ synchronized List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) {
         List<Pair<String, String>> addresses = new ArrayList<>();
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
         if (adapter == null || mLeAudio == null) {
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index 22b85d4..a9fe8cb 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -248,6 +248,25 @@
         return enabled;
     }
 
+    /**
+     * Internal version of {@link #isChangeEnabledByUid(long, int)}.
+     *
+     * <p>Does not perform costly permission check and logging.
+     */
+    public boolean isChangeEnabledByUidInternalNoLogging(long changeId, int uid) {
+        String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
+        if (packages == null || packages.length == 0) {
+            return mCompatConfig.defaultChangeIdValue(changeId);
+        }
+        boolean enabled = true;
+        final int userId = UserHandle.getUserId(uid);
+        for (String packageName : packages) {
+            final var appInfo = getApplicationInfo(packageName, userId);
+            enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo);
+        }
+        return enabled;
+    }
+
     @Override
     @EnforcePermission(OVERRIDE_COMPAT_CHANGE_CONFIG)
     public void setOverrides(CompatibilityChangeConfig overrides, String packageName) {
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java
index 133c79f..8e72546 100644
--- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java
+++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryHelper.java
@@ -24,6 +24,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
 import android.net.ConnectivityModuleConnector;
+import android.sysprop.CrashRecoveryProperties;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -35,7 +36,7 @@
 
 /**
  * Provides helper methods for the CrashRecovery APEX
- *
+ *  TODO: b/354112511 Add tests for this class when it is finalized.
  * @hide
  */
 public final class CrashRecoveryHelper {
@@ -76,11 +77,13 @@
     }
 
     /**
-     * Register health listeners for explicit package failures.
-     * Currently only registering for Connectivity Module health.
-     * @hide
+     * Register health listeners for Connectivity packages health.
+     *
+     * TODO: b/354112511 Have an internal method to trigger a rollback by reporting high severity errors,
+     * and rely on ActivityManager to inform the watchdog of severe network stack crashes
+     * instead of having this listener in parallel.
      */
-    public void registerConnectivityModuleHealthListener(@NonNull int failureReason) {
+    public void registerConnectivityModuleHealthListener() {
         // register listener for ConnectivityModule
         mConnectivityModuleConnector.registerHealthListener(
                 packageName -> {
@@ -90,7 +93,7 @@
                     return;
                 }
                 final List<VersionedPackage> pkgList = Collections.singletonList(pkg);
-                PackageWatchdog.getInstance(mContext).onPackageFailure(pkgList, failureReason);
+                PackageWatchdog.getInstance(mContext).onPackageFailure(pkgList,  PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK);
             });
     }
 
@@ -126,4 +129,21 @@
             return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
         }
     }
+
+    /**
+     * Check if we're currently attempting to reboot for a factory reset. This method must
+     * return true if RescueParty tries to reboot early during a boot loop, since the device
+     * will not be fully booted at this time.
+     */
+    public static boolean isRecoveryTriggeredReboot() {
+        return isFactoryResetPropertySet() || isRebootPropertySet();
+    }
+
+    static boolean isFactoryResetPropertySet() {
+        return CrashRecoveryProperties.attemptingFactoryReset().orElse(false);
+    }
+
+    static boolean isRebootPropertySet() {
+        return CrashRecoveryProperties.attemptingReboot().orElse(false);
+    }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 7a055d1..ed6ed60 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -99,6 +99,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.function.Function;
 
 import javax.xml.datatype.DatatypeConfigurationException;
 
@@ -1192,6 +1193,18 @@
      */
     public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) {
         Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null;
+        return getHdrBrightnessFromSdr(brightness, maxDesiredHdrSdrRatio, sdrToHdrSpline);
+    }
+
+    /**
+     * Calculate the HDR brightness for the specified SDR brightenss, restricted by the
+     * maxDesiredHdrSdrRatio (the ratio between the HDR luminance and SDR luminance) and specific
+     * sdrToHdrSpline
+     *
+     * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists.
+     */
+    public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio,
+            @Nullable Spline sdrToHdrSpline) {
         if (sdrToHdrSpline == null) {
             return PowerManager.BRIGHTNESS_INVALID;
         }
@@ -1786,15 +1799,17 @@
                 loadThermalThrottlingConfig(config);
                 loadPowerThrottlingConfigData(config);
                 // Backlight and evenDimmer data should be loaded for HbmData
-                mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config, (hbm) -> {
+                Function<HighBrightnessMode, Float> transitionPointProvider = (hbm) -> {
                     float transitionPointBacklightScale = hbm.getTransitionPoint_all().floatValue();
                     if (transitionPointBacklightScale >= mBacklightMaximum) {
                         throw new IllegalArgumentException("HBM transition point invalid. "
-                                + mHbmData.transitionPoint + " is not less than "
+                                + transitionPointBacklightScale + " is not less than "
                                 + mBacklightMaximum);
                     }
                     return  getBrightnessFromBacklight(transitionPointBacklightScale);
-                });
+                };
+                mHbmData = HighBrightnessModeData.loadHighBrightnessModeData(config,
+                        transitionPointProvider);
                 if (mHbmData.isHighBrightnessModeEnabled && mHbmData.refreshRateLimit != null) {
                     // TODO(b/331650248): cleanup, DMD can use mHbmData.refreshRateLimit
                     mRefreshRateLimitations.add(new RefreshRateLimitation(
@@ -1818,7 +1833,7 @@
                 loadRefreshRateSetting(config);
                 loadScreenOffBrightnessSensorValueToLuxFromDdc(config);
                 loadUsiVersion(config);
-                mHdrBrightnessData = HdrBrightnessData.loadConfig(config);
+                mHdrBrightnessData = HdrBrightnessData.loadConfig(config, transitionPointProvider);
                 loadBrightnessCapForWearBedtimeMode(config);
                 loadIdleScreenRefreshRateTimeoutConfigs(config);
                 mVrrSupportEnabled = config.getSupportsVrr();
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b3a6c1c..2cec869 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3849,9 +3849,10 @@
             // Ignore redundant events. Further optimization is possible by merging adjacent events.
             Pair<Integer, Integer> last = mDisplayEvents.get(mDisplayEvents.size() - 1);
             if (last.first == displayId && last.second == event) {
-                Slog.d(TAG,
-                        "Ignore redundant display event " + displayId + "/" + event + " to "
-                                + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid);
+                if (DEBUG) {
+                    Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event + " to "
+                            + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid);
+                }
                 return;
             }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 5c1e783..1177be2 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -129,9 +129,9 @@
     private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
-    private static final String TAG = "DisplayPowerController2";
+    private static final String TAG = "DisplayPowerController";
     // To enable these logs, run:
-    // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
+    // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot'
     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
     private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME =
             "Screen on blocked by displayoffload";
@@ -263,6 +263,7 @@
 
     // The unique ID of the primary display device currently tied to this logical display
     private String mUniqueDisplayId;
+    private String mPhysicalDisplayName;
 
     // Tracker for brightness changes.
     @Nullable
@@ -371,10 +372,6 @@
     // If the last recorded screen state was dozing or not.
     private boolean mDozing;
 
-    private boolean mAppliedDimming;
-
-    private boolean mAppliedThrottling;
-
     // Reason for which the brightness was last changed. See {@link BrightnessReason} for more
     // information.
     // At the time of this writing, this value is changed within updatePowerState() only, which is
@@ -483,7 +480,7 @@
     // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there
     // is one lead display, the additional displays follow the brightness value of the lead display.
     @GuardedBy("mLock")
-    private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
+    private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers =
             new SparseArray();
 
     private boolean mBootCompleted;
@@ -508,14 +505,15 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
+        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
+        IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+        DisplayDeviceInfo displayDeviceInfo = mDisplayDevice.getDisplayDeviceInfoLocked();
         mSensorManager = sensorManager;
         mHandler = new DisplayControllerHandler(handler.getLooper());
-        mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceConfig();
+        mDisplayDeviceConfig = mDisplayDevice.getDisplayDeviceConfig();
         mIsEnabled = logicalDisplay.isEnabledLocked();
         mIsInTransition = logicalDisplay.isInTransitionLocked();
-        mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked()
-                .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL;
+        mIsDisplayInternal = displayDeviceInfo.type == Display.TYPE_INTERNAL;
         mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks);
         mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
@@ -524,9 +522,10 @@
         mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
-        mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+
+        mUniqueDisplayId = mDisplayDevice.getUniqueId();
         mDisplayStatsId = mUniqueDisplayId.hashCode();
+        mPhysicalDisplayName = mDisplayDevice.getNameLocked();
 
         mLastBrightnessEvent = new BrightnessEvent(mDisplayId);
         mTempBrightnessEvent = new BrightnessEvent(mDisplayId);
@@ -544,8 +543,6 @@
         mBrightnessTracker = brightnessTracker;
         mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
 
-        PowerManager pm = context.getSystemService(PowerManager.class);
-
         final Resources resources = context.getResources();
 
         // DOZE AND DIM SETTINGS
@@ -573,8 +570,7 @@
 
         mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
                 modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
-                mDisplayDevice.getDisplayTokenLocked(),
-                mDisplayDevice.getDisplayDeviceInfoLocked());
+                displayToken, displayDeviceInfo);
 
         mDisplayBrightnessController =
                 new DisplayBrightnessController(context, null,
@@ -588,8 +584,8 @@
                         mUniqueDisplayId,
                         mThermalBrightnessThrottlingDataId,
                         logicalDisplay.getPowerThrottlingDataIdLocked(),
-                        mDisplayDeviceConfig,
-                        mDisplayId), mContext, flags, mSensorManager);
+                        mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height,
+                        displayToken, mDisplayId), mContext, flags, mSensorManager);
         // Seed the cached brightness
         saveBrightnessInfo(getScreenBrightnessSetting());
         mAutomaticBrightnessStrategy =
@@ -840,6 +836,7 @@
         }
 
         final String uniqueId = device.getUniqueId();
+        final String displayName = device.getNameLocked();
         final DisplayDeviceConfig config = device.getDisplayDeviceConfig();
         final IBinder token = device.getDisplayTokenLocked();
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
@@ -866,6 +863,7 @@
                 changed = true;
                 mDisplayDevice = device;
                 mUniqueDisplayId = uniqueId;
+                mPhysicalDisplayName = displayName;
                 mDisplayStatsId = mUniqueDisplayId.hashCode();
                 mDisplayDeviceConfig = config;
                 mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
@@ -895,7 +893,7 @@
             mBrightnessClamperController.onDisplayChanged(
                     new BrightnessClamperController.DisplayDeviceData(uniqueId,
                             thermalBrightnessThrottlingDataId, powerThrottlingDataId,
-                            config, mDisplayId));
+                            config, info.width, info.height, token, mDisplayId));
 
             if (changed) {
                 updatePowerState();
@@ -1552,10 +1550,9 @@
         // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations.
         // Note throttling effectively changes the allowed brightness range, so, similarly to HBM,
         // we broadcast this change through setting.
-        final float unthrottledBrightnessState = brightnessState;
+        final float unthrottledBrightnessState = rawBrightnessState;
         DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest,
                 brightnessState, slowChange, /* displayState= */ state);
-
         brightnessState = clampedState.getBrightness();
         slowChange = clampedState.isSlowChange();
         // faster rate wins, at this point customAnimationRate == -1, strategy does not control
@@ -1744,11 +1741,23 @@
         // brightness cap, RBC state, etc.
         mTempBrightnessEvent.setTime(System.currentTimeMillis());
         mTempBrightnessEvent.setBrightness(brightnessState);
+        mTempBrightnessEvent.setNits(
+                mDisplayBrightnessController.convertToAdjustedNits(brightnessState));
+        final float hbmMax = mBrightnessRangeController.getCurrentBrightnessMax();
+        final float clampedMax = Math.min(clampedState.getMaxBrightness(), hbmMax);
+        final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f,
+                clampedState.getMinBrightness(), clampedMax,
+                brightnessState);
+        mTempBrightnessEvent.setPercent(Math.round(
+                1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma(
+                        brightnessOnAvailableScale) / 10)); // rounded to one dp
+        mTempBrightnessEvent.setUnclampedBrightness(unthrottledBrightnessState);
         mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);
+        mTempBrightnessEvent.setPhysicalDisplayName(mPhysicalDisplayName);
         mTempBrightnessEvent.setDisplayState(state);
         mTempBrightnessEvent.setDisplayPolicy(mPowerRequest.policy);
         mTempBrightnessEvent.setReason(mBrightnessReason);
-        mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax());
+        mTempBrightnessEvent.setHbmMax(hbmMax);
         mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
         mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
                 | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
@@ -2648,8 +2657,6 @@
         pw.println("Display Power Controller Thread State:");
         pw.println("  mPowerRequest=" + mPowerRequest);
         pw.println("  mBrightnessReason=" + mBrightnessReason);
-        pw.println("  mAppliedDimming=" + mAppliedDimming);
-        pw.println("  mAppliedThrottling=" + mAppliedThrottling);
         pw.println("  mDozing=" + mDozing);
         pw.println("  mSkipRampState=" + skipRampStateToString(mSkipRampState));
         pw.println("  mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime);
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index 82b401a..5cc603c 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -20,6 +20,8 @@
 import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.policyToString;
 
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
+import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX;
+import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS;
 import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessModeToString;
 
 import android.hardware.display.BrightnessInfo;
@@ -48,13 +50,17 @@
     private BrightnessReason mReason = new BrightnessReason();
     private int mDisplayId;
     private String mPhysicalDisplayId;
+    private String mPhysicalDisplayName;
     private int mDisplayState;
     private int mDisplayPolicy;
     private long mTime;
     private float mLux;
+    private float mNits;
+    private float mPercent;
     private float mPreThresholdLux;
     private float mInitialBrightness;
     private float mBrightness;
+    private float mUnclampedBrightness;
     private float mRecommendedBrightness;
     private float mPreThresholdBrightness;
     private int mHbmMode;
@@ -88,15 +94,19 @@
         mReason.set(that.getReason());
         mDisplayId = that.getDisplayId();
         mPhysicalDisplayId = that.getPhysicalDisplayId();
+        mPhysicalDisplayName = that.getPhysicalDisplayName();
         mDisplayState = that.mDisplayState;
         mDisplayPolicy = that.mDisplayPolicy;
         mTime = that.getTime();
         // Lux values
         mLux = that.getLux();
         mPreThresholdLux = that.getPreThresholdLux();
+        mNits = that.getNits();
+        mPercent = that.getPercent();
         // Brightness values
         mInitialBrightness = that.getInitialBrightness();
         mBrightness = that.getBrightness();
+        mUnclampedBrightness = that.getUnclampedBrightness();
         mRecommendedBrightness = that.getRecommendedBrightness();
         mPreThresholdBrightness = that.getPreThresholdBrightness();
         // Different brightness modulations
@@ -121,14 +131,18 @@
         mReason = new BrightnessReason();
         mTime = SystemClock.uptimeMillis();
         mPhysicalDisplayId = "";
+        mPhysicalDisplayName = "";
         mDisplayState = Display.STATE_UNKNOWN;
         mDisplayPolicy = POLICY_OFF;
         // Lux values
-        mLux = 0;
+        mLux = INVALID_LUX;
         mPreThresholdLux = 0;
+        mNits = INVALID_NITS;
+        mPercent = -1f;
         // Brightness values
         mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+        mUnclampedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         mRecommendedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         // Different brightness modulations
@@ -160,13 +174,18 @@
         return mReason.equals(that.mReason)
                 && mDisplayId == that.mDisplayId
                 && mPhysicalDisplayId.equals(that.mPhysicalDisplayId)
+                && mPhysicalDisplayName.equals(that.mPhysicalDisplayName)
                 && mDisplayState == that.mDisplayState
                 && mDisplayPolicy == that.mDisplayPolicy
                 && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux)
                 && Float.floatToRawIntBits(mPreThresholdLux)
                 == Float.floatToRawIntBits(that.mPreThresholdLux)
+                && Float.floatToRawIntBits(mNits) == Float.floatToRawIntBits(that.mNits)
+                && Float.floatToRawIntBits(mPercent) == Float.floatToRawIntBits(that.mPercent)
                 && Float.floatToRawIntBits(mBrightness)
                 == Float.floatToRawIntBits(that.mBrightness)
+                && Float.floatToRawIntBits(mUnclampedBrightness)
+                == Float.floatToRawIntBits(that.mUnclampedBrightness)
                 && Float.floatToRawIntBits(mRecommendedBrightness)
                 == Float.floatToRawIntBits(that.mRecommendedBrightness)
                 && Float.floatToRawIntBits(mPreThresholdBrightness)
@@ -195,27 +214,34 @@
     public String toString(boolean includeTime) {
         return (includeTime ? FORMAT.format(new Date(mTime)) + " - " : "")
                 + "BrightnessEvent: "
-                + "disp=" + mDisplayId
-                + ", physDisp=" + mPhysicalDisplayId
-                + ", displayState=" + Display.stateToString(mDisplayState)
-                + ", displayPolicy=" + policyToString(mDisplayPolicy)
-                + ", brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "")
+                + "brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + " ("
+                + mPercent + "%)"
+                + ", nits= " + mNits
+                + ", lux=" + mLux
+                + ", reason=" + mReason.toString(mAdjustmentFlags)
+                + ", strat=" + mDisplayBrightnessStrategyName
+                + ", state=" + Display.stateToString(mDisplayState)
+                + ", policy=" + policyToString(mDisplayPolicy)
+                + ", flags=" + flagsToString()
+                // Autobrightness
                 + ", initBrt=" + mInitialBrightness
                 + ", rcmdBrt=" + mRecommendedBrightness
                 + ", preBrt=" + mPreThresholdBrightness
-                + ", lux=" + mLux
                 + ", preLux=" + mPreThresholdLux
+                + ", wasShortTermModelActive=" + mWasShortTermModelActive
+                + ", autoBrightness=" + mAutomaticBrightnessEnabled + " ("
+                + autoBrightnessModeToString(mAutoBrightnessMode) + ")"
+                // Throttling info
+                + ", unclampedBrt=" + mUnclampedBrightness
                 + ", hbmMax=" + mHbmMax
                 + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode)
-                + ", rbcStrength=" + mRbcStrength
                 + ", thrmMax=" + mThermalMax
+                // Modifiers
+                + ", rbcStrength=" + mRbcStrength
                 + ", powerFactor=" + mPowerFactor
-                + ", wasShortTermModelActive=" + mWasShortTermModelActive
-                + ", flags=" + flagsToString()
-                + ", reason=" + mReason.toString(mAdjustmentFlags)
-                + ", autoBrightness=" + mAutomaticBrightnessEnabled
-                + ", strategy=" + mDisplayBrightnessStrategyName
-                + ", autoBrightnessMode=" + autoBrightnessModeToString(mAutoBrightnessMode);
+                // Meta
+                + ", physDisp=" + mPhysicalDisplayName + "(" + mPhysicalDisplayId + ")"
+                + ", logicalId=" + mDisplayId;
     }
 
     @Override
@@ -255,6 +281,14 @@
         this.mPhysicalDisplayId = mPhysicalDisplayId;
     }
 
+    public String getPhysicalDisplayName() {
+        return mPhysicalDisplayName;
+    }
+
+    public void setPhysicalDisplayName(String mPhysicalDisplayName) {
+        this.mPhysicalDisplayName = mPhysicalDisplayName;
+    }
+
     public void setDisplayState(int state) {
         mDisplayState = state;
     }
@@ -295,6 +329,29 @@
         this.mBrightness = brightness;
     }
 
+    public float getUnclampedBrightness() {
+        return mUnclampedBrightness;
+    }
+
+    public void setUnclampedBrightness(float unclampedBrightness) {
+        this.mUnclampedBrightness = unclampedBrightness;
+    }
+
+    public void setPercent(float percent) {
+        this.mPercent = percent;
+    }
+    public float getPercent() {
+        return mPercent;
+    }
+
+    public void setNits(float nits) {
+        this.mNits = nits;
+    }
+
+    public float getNits() {
+        return mNits;
+    }
+
     public float getRecommendedBrightness() {
         return mRecommendedBrightness;
     }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 88d2c00..9324fc1 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -28,13 +28,16 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Handler;
 import android.os.HandlerExecutor;
+import android.os.IBinder;
 import android.os.PowerManager;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
+import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.DisplayBrightnessState;
 import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData;
@@ -65,6 +68,11 @@
     private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers;
 
     private final List<BrightnessStateModifier> mModifiers;
+
+    private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>();
+    private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>();
+    private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState();
+
     private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
 
@@ -110,7 +118,16 @@
         mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags,
                 context);
         mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener,
-                data.mDisplayDeviceConfig);
+                data);
+
+        mModifiers.forEach(m -> {
+            if (m instanceof  DisplayDeviceDataListener l) {
+                mDisplayDeviceDataListeners.add(l);
+            }
+            if (m instanceof StatefulModifier s) {
+                mStatefulModifiers.add(s);
+            }
+        });
         mOnPropertiesChangedListener =
                 properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged);
         mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
@@ -123,6 +140,7 @@
     public void onDisplayChanged(DisplayDeviceData data) {
         mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId());
         mClampers.forEach(clamper -> clamper.onDisplayChanged(data));
+        mDisplayDeviceDataListeners.forEach(l -> l.onDisplayChanged(data));
         adjustLightSensorSubscription();
     }
 
@@ -234,14 +252,28 @@
             customAnimationRate = minClamper.getCustomAnimationRate();
         }
 
+        ModifiersAggregatedState newAggregatedState = new ModifiersAggregatedState();
+        mStatefulModifiers.forEach((clamper) -> clamper.applyStateChange(newAggregatedState));
+
         if (mBrightnessCap != brightnessCap
                 || mClamperType != clamperType
-                || mCustomAnimationRate != customAnimationRate) {
+                || mCustomAnimationRate != customAnimationRate
+                || needToNotifyExternalListener(mModifiersAggregatedState, newAggregatedState)) {
             mBrightnessCap = brightnessCap;
             mClamperType = clamperType;
             mCustomAnimationRate = customAnimationRate;
             mClamperChangeListenerExternal.onChanged();
         }
+        mModifiersAggregatedState = newAggregatedState;
+    }
+
+    private boolean needToNotifyExternalListener(ModifiersAggregatedState state1,
+            ModifiersAggregatedState state2) {
+        return !BrightnessSynchronizer.floatEquals(state1.mMaxDesiredHdrRatio,
+                state2.mMaxDesiredHdrRatio)
+                || !BrightnessSynchronizer.floatEquals(state1.mMaxHdrBrightness,
+                state2.mMaxHdrBrightness)
+                || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline;
     }
 
     private void start() {
@@ -295,17 +327,16 @@
 
         List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
                 Handler handler, ClamperChangeListener listener,
-                DisplayDeviceConfig displayDeviceConfig) {
+                DisplayDeviceData data) {
             List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
-            if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null
-                    && displayDeviceConfig.isEvenDimmerAvailable()) {
+            if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) {
                 modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
-                        displayDeviceConfig));
+                        data.mDisplayDeviceConfig));
             }
             if (flags.useNewHdrBrightnessModifier()) {
-                modifiers.add(new HdrBrightnessModifier());
+                modifiers.add(new HdrBrightnessModifier(handler, listener, data));
             }
             return modifiers;
         }
@@ -319,7 +350,14 @@
     }
 
     /**
-     * Config Data for clampers
+     * Modifier should implement this interface in order to receive display change updates
+     */
+    interface DisplayDeviceDataListener {
+        void onDisplayChanged(DisplayDeviceData displayData);
+    }
+
+    /**
+     * Config Data for clampers/modifiers
      */
     public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData,
             BrightnessPowerClamper.PowerData,
@@ -331,23 +369,34 @@
         @NonNull
         private final String mPowerThrottlingDataId;
         @NonNull
-        private final DisplayDeviceConfig mDisplayDeviceConfig;
+        final DisplayDeviceConfig mDisplayDeviceConfig;
 
-        private final int mDisplayId;
+        final int mWidth;
+
+        final int mHeight;
+
+        final IBinder mDisplayToken;
+
+        final int mDisplayId;
 
         public DisplayDeviceData(@NonNull String uniqueDisplayId,
                 @NonNull String thermalThrottlingDataId,
                 @NonNull String powerThrottlingDataId,
                 @NonNull DisplayDeviceConfig displayDeviceConfig,
+                int width,
+                int height,
+                IBinder displayToken,
                 int displayId) {
             mUniqueDisplayId = uniqueDisplayId;
             mThermalThrottlingDataId = thermalThrottlingDataId;
             mPowerThrottlingDataId = powerThrottlingDataId;
             mDisplayDeviceConfig = displayDeviceConfig;
+            mWidth = width;
+            mHeight = height;
+            mDisplayToken = displayToken;
             mDisplayId = displayId;
         }
 
-
         @NonNull
         @Override
         public String getUniqueDisplayId() {
@@ -406,4 +455,24 @@
             return mDisplayId;
         }
     }
+
+    /**
+     * Stateful modifier should implement this interface and modify aggregatedState.
+     * AggregatedState is used by Controller to determine if updatePowerState call is needed
+     * to correctly adjust brightness
+     */
+    interface StatefulModifier {
+        void applyStateChange(ModifiersAggregatedState aggregatedState);
+    }
+
+    /**
+     * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness
+     * adjustement is needed
+     */
+    public static class ModifiersAggregatedState {
+        float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO;
+        float mMaxHdrBrightness = PowerManager.BRIGHTNESS_MAX;
+        @Nullable
+        Spline mSdrHdrRatioSpline = null;
+    }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
index a829866..5e44cc3 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java
@@ -16,36 +16,325 @@
 
 package com.android.server.display.brightness.clamper;
 
-import android.hardware.display.DisplayManagerInternal;
+import static com.android.server.display.DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
+import static com.android.server.display.brightness.clamper.LightSensorController.INVALID_LUX;
 
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.hardware.display.DisplayManagerInternal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.view.SurfaceControlHdrLayerInfoListener;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.server.display.DisplayBrightnessState;
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.config.HdrBrightnessData;
 
 import java.io.PrintWriter;
+import java.util.Map;
 
-public class HdrBrightnessModifier implements BrightnessStateModifier {
+public class HdrBrightnessModifier implements BrightnessStateModifier,
+        BrightnessClamperController.DisplayDeviceDataListener,
+        BrightnessClamperController.StatefulModifier {
+
+    static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f;
+    private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f;
+
+    private final SurfaceControlHdrLayerInfoListener mHdrListener =
+            new SurfaceControlHdrLayerInfoListener() {
+                @Override
+                public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW,
+                        int maxH, int flags, float maxDesiredHdrSdrRatio) {
+                    boolean hdrLayerPresent = numberOfHdrLayers > 0;
+                    mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged(
+                            hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE,
+                            hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO));
+                }
+            };
+
+    private final Handler mHandler;
+    private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener;
+    private final Injector mInjector;
+    private final Runnable mDebouncer;
+
+    private IBinder mRegisteredDisplayToken;
+
+    private DisplayDeviceConfig mDisplayDeviceConfig;
+    @Nullable
+    private HdrBrightnessData mHdrBrightnessData;
+    private float mScreenSize;
+
+    private float mMaxDesiredHdrRatio = DEFAULT_MAX_HDR_SDR_RATIO;
+    private float mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+
+    private float mAmbientLux = INVALID_LUX;
+
+    private Mode mMode = Mode.NO_HDR;
+    // The maximum brightness allowed for current lux
+    private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+    private float mPendingMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+    // brightness change speed, in units per seconds. Applied only on ambient lux changes
+    private float mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+    private float mPendingTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+
+    HdrBrightnessModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+            BrightnessClamperController.DisplayDeviceData displayData) {
+        this(new Handler(handler.getLooper()), clamperChangeListener, new Injector(), displayData);
+    }
+
+    @VisibleForTesting
+    HdrBrightnessModifier(Handler handler,
+            BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+            Injector injector,
+            BrightnessClamperController.DisplayDeviceData displayData) {
+        mHandler = handler;
+        mClamperChangeListener = clamperChangeListener;
+        mInjector = injector;
+        mDebouncer = () -> {
+            mTransitionRate = mPendingTransitionRate;
+            mMaxBrightness = mPendingMaxBrightness;
+            mClamperChangeListener.onChanged();
+        };
+        onDisplayChanged(displayData);
+    }
+
+    // Called in DisplayControllerHandler
     @Override
     public void apply(DisplayManagerInternal.DisplayPowerRequest request,
             DisplayBrightnessState.Builder stateBuilder) {
-        // noop
+        if (mHdrBrightnessData  == null) { // no hdr data
+            return;
+        }
+        if (mMode == Mode.NO_HDR) {
+            return;
+        }
+        float hdrBrightness = mDisplayDeviceConfig.getHdrBrightnessFromSdr(
+                stateBuilder.getBrightness(), mMaxDesiredHdrRatio,
+                mHdrBrightnessData.sdrToHdrRatioSpline);
+        float maxBrightness = getMaxBrightness(mMode, mMaxBrightness, mHdrBrightnessData);
+        hdrBrightness = Math.min(hdrBrightness, maxBrightness);
+
+        stateBuilder.setHdrBrightness(hdrBrightness);
+        stateBuilder.setCustomAnimationRate(mTransitionRate);
+        // transition rate applied, reset
+        mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
     }
 
     @Override
-    public void dump(PrintWriter printWriter) {
-        // noop
+    public void dump(PrintWriter pw) {
+        pw.println("HdrBrightnessModifier:");
+        pw.println("  mHdrBrightnessData=" + mHdrBrightnessData);
+        pw.println("  mScreenSize=" + mScreenSize);
+        pw.println("  mMaxDesiredHdrRatio=" + mMaxDesiredHdrRatio);
+        pw.println("  mHdrLayerSize=" + mHdrLayerSize);
+        pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mMode=" + mMode);
+        pw.println("  mMaxBrightness=" + mMaxBrightness);
+        pw.println("  mPendingMaxBrightness=" + mPendingMaxBrightness);
+        pw.println("  mTransitionRate=" + mTransitionRate);
+        pw.println("  mPendingTransitionRate=" + mPendingTransitionRate);
+        pw.println("  mHdrListener registered=" + (mRegisteredDisplayToken != null));
     }
 
+    // Called in DisplayControllerHandler
     @Override
     public void stop() {
-        // noop
+        unregisterHdrListener();
+        mHandler.removeCallbacksAndMessages(null);
     }
 
+    // Called in DisplayControllerHandler
     @Override
     public boolean shouldListenToLightSensor() {
-        return false;
+        return hasBrightnessLimits();
+    }
+
+    // Called in DisplayControllerHandler
+    @Override
+    public void setAmbientLux(float lux) {
+        mAmbientLux = lux;
+        if (!hasBrightnessLimits()) {
+            return;
+        }
+        float desiredMaxBrightness = findBrightnessLimit(mHdrBrightnessData, lux);
+        if (mMode == Mode.NO_HDR) {
+            mMaxBrightness = desiredMaxBrightness;
+        } else {
+            scheduleMaxBrightnessUpdate(desiredMaxBrightness, mHdrBrightnessData);
+        }
     }
 
     @Override
-    public void setAmbientLux(float lux) {
-        // noop
+    public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) {
+        mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth,
+                displayData.mHeight, displayData.mDisplayDeviceConfig));
+    }
+
+    // Called in DisplayControllerHandler, when any modifier state changes
+    @Override
+    public void applyStateChange(
+            BrightnessClamperController.ModifiersAggregatedState aggregatedState) {
+        if (mMode != Mode.NO_HDR && mHdrBrightnessData != null) {
+            aggregatedState.mMaxDesiredHdrRatio = mMaxDesiredHdrRatio;
+            aggregatedState.mSdrHdrRatioSpline = mHdrBrightnessData.sdrToHdrRatioSpline;
+            aggregatedState.mMaxHdrBrightness = getMaxBrightness(
+                    mMode, mMaxBrightness, mHdrBrightnessData);
+        }
+    }
+
+    private boolean hasBrightnessLimits() {
+        return mHdrBrightnessData != null && !mHdrBrightnessData.maxBrightnessLimits.isEmpty();
+    }
+
+    private void scheduleMaxBrightnessUpdate(float desiredMaxBrightness, HdrBrightnessData data) {
+        if (mMaxBrightness == desiredMaxBrightness) {
+            mPendingMaxBrightness = mMaxBrightness;
+            mPendingTransitionRate = -1f;
+            mTransitionRate = -1f;
+            mHandler.removeCallbacks(mDebouncer);
+        } else if (mPendingMaxBrightness != desiredMaxBrightness) {
+            mPendingMaxBrightness = desiredMaxBrightness;
+            long debounceTime;
+            if (mPendingMaxBrightness > mMaxBrightness) {
+                debounceTime = data.brightnessIncreaseDebounceMillis;
+                mPendingTransitionRate = data.screenBrightnessRampIncrease;
+            } else {
+                debounceTime = data.brightnessDecreaseDebounceMillis;
+                mPendingTransitionRate = data.screenBrightnessRampDecrease;
+            }
+
+            mHandler.removeCallbacks(mDebouncer);
+            mHandler.postDelayed(mDebouncer, debounceTime);
+        }
+        // do nothing if expectedMaxBrightness == mDesiredMaxBrightness
+        // && expectedMaxBrightness != mMaxBrightness
+    }
+
+    // Called in DisplayControllerHandler
+    private void onDisplayChanged(IBinder displayToken, int width, int height,
+            DisplayDeviceConfig config) {
+        mDisplayDeviceConfig = config;
+        mScreenSize = (float) width * height;
+        HdrBrightnessData data = config.getHdrBrightnessData();
+        if (data == null) {
+            unregisterHdrListener();
+        } else {
+            registerHdrListener(displayToken);
+        }
+        recalculate(data, mMaxDesiredHdrRatio);
+    }
+
+    // Called in DisplayControllerHandler
+    private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) {
+        Mode newMode = recalculateMode(data);
+        // if HDR mode changed, notify changed
+        boolean needToNotifyChange = mMode != newMode;
+        // If HDR mode is active, we need to check if other HDR params are changed
+        if (mMode != HdrBrightnessModifier.Mode.NO_HDR) {
+            if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio)
+                    || data != mHdrBrightnessData) {
+                needToNotifyChange = true;
+            }
+        }
+
+        mMode = newMode;
+        mHdrBrightnessData = data;
+        mMaxDesiredHdrRatio = maxDesiredHdrRatio;
+
+        if (needToNotifyChange) {
+            // data or hdr layer changed, reset custom transition rate
+            mTransitionRate = CUSTOM_ANIMATION_RATE_NOT_SET;
+            mClamperChangeListener.onChanged();
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private Mode recalculateMode(@Nullable HdrBrightnessData data) {
+        // no config
+        if (data == null) {
+            return Mode.NO_HDR;
+        }
+        // HDR layer < minHdr % for Nbm
+        if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) {
+            return Mode.NO_HDR;
+        }
+        // HDR layer < minHdr % for Hbm, and HDR layer >= that minHdr % for Nbm
+        if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForHbm) {
+            return Mode.NBM_HDR;
+        }
+        // HDR layer > that minHdr % for Hbm
+        return Mode.HBM_HDR;
+    }
+
+    private float getMaxBrightness(Mode mode, float maxBrightness, HdrBrightnessData data) {
+        if (mode == Mode.NBM_HDR) {
+            return Math.min(data.hbmTransitionPoint, maxBrightness);
+        } else if (mode == Mode.HBM_HDR) {
+            return maxBrightness;
+        } else {
+            return PowerManager.BRIGHTNESS_MAX;
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private float findBrightnessLimit(HdrBrightnessData data, float ambientLux) {
+        float foundAmbientBoundary = Float.MAX_VALUE;
+        float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+        for (Map.Entry<Float, Float> brightnessPoint :
+                data.maxBrightnessLimits.entrySet()) {
+            float ambientBoundary = brightnessPoint.getKey();
+            // find ambient lux upper boundary closest to current ambient lux
+            if (ambientBoundary > ambientLux && ambientBoundary < foundAmbientBoundary) {
+                foundMaxBrightness = brightnessPoint.getValue();
+                foundAmbientBoundary = ambientBoundary;
+            }
+        }
+        return foundMaxBrightness;
+    }
+
+    // Called in DisplayControllerHandler
+    private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) {
+        mHdrLayerSize = hdrLayerSize;
+        recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio);
+    }
+
+    // Called in DisplayControllerHandler
+    private void registerHdrListener(IBinder displayToken) {
+        if (mRegisteredDisplayToken == displayToken) {
+            return;
+        }
+        unregisterHdrListener();
+        if (displayToken != null) {
+            mInjector.registerHdrListener(mHdrListener, displayToken);
+            mRegisteredDisplayToken = displayToken;
+        }
+    }
+
+    // Called in DisplayControllerHandler
+    private void unregisterHdrListener() {
+        if (mRegisteredDisplayToken != null) {
+            mInjector.unregisterHdrListener(mHdrListener, mRegisteredDisplayToken);
+            mRegisteredDisplayToken = null;
+            mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE;
+        }
+    }
+
+    private enum Mode {
+        NO_HDR, NBM_HDR, HBM_HDR
+    }
+
+    @SuppressLint("MissingPermission")
+    static class Injector {
+        void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+            listener.register(token);
+        }
+
+        void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) {
+            listener.unregister(token);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/config/HdrBrightnessData.java b/services/core/java/com/android/server/display/config/HdrBrightnessData.java
index c940807..ef4a798 100644
--- a/services/core/java/com/android/server/display/config/HdrBrightnessData.java
+++ b/services/core/java/com/android/server/display/config/HdrBrightnessData.java
@@ -19,6 +19,7 @@
 import static com.android.server.display.config.HighBrightnessModeData.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
 
 import android.annotation.Nullable;
+import android.os.PowerManager;
 import android.util.Spline;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -29,6 +30,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 
 /**
  * Brightness config for HDR content
@@ -48,9 +50,9 @@
  *             </point>
  *         </brightnessMap>
  *         <brightnessIncreaseDebounceMillis>1000</brightnessIncreaseDebounceMillis>
- *         <brightnessIncreaseDurationMillis>10000</brightnessIncreaseDurationMillis>
+ *         <screenBrightnessRampIncrease>0.04</brightnessIncreaseDurationMillis>
  *         <brightnessDecreaseDebounceMillis>13000</brightnessDecreaseDebounceMillis>
- *         <brightnessDecreaseDurationMillis>10000</brightnessDecreaseDurationMillis>
+ *         <screenBrightnessRampDecrease>0.03</brightnessDecreaseDurationMillis>
  *         <minimumHdrPercentOfScreenForNbm>0.2</minimumHdrPercentOfScreenForNbm>
  *         <minimumHdrPercentOfScreenForHbm>0.5</minimumHdrPercentOfScreenForHbm>
  *         <allowInLowPowerMode>true</allowInLowPowerMode>
@@ -99,6 +101,11 @@
     public final float screenBrightnessRampDecrease;
 
     /**
+     * Brightness level at which we transition from normal to high-brightness
+     */
+    public final float hbmTransitionPoint;
+
+    /**
      * Min Hdr layer size to start hdr brightness boost up to high brightness mode transition point
      */
     public final float minimumHdrPercentOfScreenForNbm;
@@ -123,6 +130,7 @@
     public HdrBrightnessData(Map<Float, Float> maxBrightnessLimits,
             long brightnessIncreaseDebounceMillis, float screenBrightnessRampIncrease,
             long brightnessDecreaseDebounceMillis, float screenBrightnessRampDecrease,
+            float hbmTransitionPoint,
             float minimumHdrPercentOfScreenForNbm, float minimumHdrPercentOfScreenForHbm,
             boolean allowInLowPowerMode, @Nullable Spline sdrToHdrRatioSpline) {
         this.maxBrightnessLimits = maxBrightnessLimits;
@@ -130,6 +138,7 @@
         this.screenBrightnessRampIncrease = screenBrightnessRampIncrease;
         this.brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis;
         this.screenBrightnessRampDecrease = screenBrightnessRampDecrease;
+        this.hbmTransitionPoint = hbmTransitionPoint;
         this.minimumHdrPercentOfScreenForNbm = minimumHdrPercentOfScreenForNbm;
         this.minimumHdrPercentOfScreenForHbm = minimumHdrPercentOfScreenForHbm;
         this.allowInLowPowerMode = allowInLowPowerMode;
@@ -144,6 +153,7 @@
                 + ", mScreenBrightnessRampIncrease: " + screenBrightnessRampIncrease
                 + ", mBrightnessDecreaseDebounceMillis: " + brightnessDecreaseDebounceMillis
                 + ", mScreenBrightnessRampDecrease: " + screenBrightnessRampDecrease
+                + ", transitionPoint: " + hbmTransitionPoint
                 + ", minimumHdrPercentOfScreenForNbm: " + minimumHdrPercentOfScreenForNbm
                 + ", minimumHdrPercentOfScreenForHbm: " + minimumHdrPercentOfScreenForHbm
                 + ", allowInLowPowerMode: " + allowInLowPowerMode
@@ -155,10 +165,12 @@
      * Loads HdrBrightnessData from DisplayConfiguration
      */
     @Nullable
-    public static HdrBrightnessData loadConfig(DisplayConfiguration config) {
+    public static HdrBrightnessData loadConfig(DisplayConfiguration config,
+            Function<HighBrightnessMode, Float> transitionPointProvider) {
+        HighBrightnessMode hbmConfig = config.getHighBrightnessMode();
         HdrBrightnessConfig hdrConfig = config.getHdrBrightnessConfig();
         if (hdrConfig == null) {
-            return getFallbackData(config.getHighBrightnessMode());
+            return getFallbackData(hbmConfig, transitionPointProvider);
         }
 
         List<NonNegativeFloatToFloatPoint> points = hdrConfig.getBrightnessMap().getPoint();
@@ -169,22 +181,38 @@
 
         float minHdrPercentForHbm = hdrConfig.getMinimumHdrPercentOfScreenForHbm() != null
                 ? hdrConfig.getMinimumHdrPercentOfScreenForHbm().floatValue()
-                : getFallbackHdrPercent(config.getHighBrightnessMode());
+                : getFallbackHdrPercent(hbmConfig);
 
         float minHdrPercentForNbm = hdrConfig.getMinimumHdrPercentOfScreenForNbm() != null
                 ? hdrConfig.getMinimumHdrPercentOfScreenForNbm().floatValue() : minHdrPercentForHbm;
 
+        if (minHdrPercentForNbm > minHdrPercentForHbm) {
+            throw new IllegalArgumentException(
+                    "minHdrPercentForHbm should be >= minHdrPercentForNbm");
+        }
+
         return new HdrBrightnessData(brightnessLimits,
                 hdrConfig.getBrightnessIncreaseDebounceMillis().longValue(),
                 hdrConfig.getScreenBrightnessRampIncrease().floatValue(),
                 hdrConfig.getBrightnessDecreaseDebounceMillis().longValue(),
                 hdrConfig.getScreenBrightnessRampDecrease().floatValue(),
+                getTransitionPoint(hbmConfig, transitionPointProvider),
                 minHdrPercentForNbm, minHdrPercentForHbm, hdrConfig.getAllowInLowPowerMode(),
                 getSdrHdrRatioSpline(hdrConfig, config.getHighBrightnessMode()));
     }
 
+    private static float getTransitionPoint(@Nullable HighBrightnessMode hbm,
+            Function<HighBrightnessMode, Float> transitionPointProvider) {
+        if (hbm == null) {
+            return PowerManager.BRIGHTNESS_MAX;
+        } else {
+            return transitionPointProvider.apply(hbm);
+        }
+    }
+
     @Nullable
-    private static HdrBrightnessData getFallbackData(HighBrightnessMode hbm) {
+    private static HdrBrightnessData getFallbackData(@Nullable HighBrightnessMode hbm,
+            Function<HighBrightnessMode, Float> transitionPointProvider) {
         if (hbm == null) {
             return null;
         }
@@ -193,6 +221,7 @@
         return new HdrBrightnessData(Collections.emptyMap(),
                 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET,
                 0, DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET,
+                getTransitionPoint(hbm, transitionPointProvider),
                 fallbackPercent, fallbackPercent, false, fallbackSpline);
     }
 
diff --git a/services/core/java/com/android/server/display/config/SensorData.java b/services/core/java/com/android/server/display/config/SensorData.java
index 8bfc4a3..1437c8d 100644
--- a/services/core/java/com/android/server/display/config/SensorData.java
+++ b/services/core/java/com/android/server/display/config/SensorData.java
@@ -34,6 +34,8 @@
 
     public static final String TEMPERATURE_TYPE_DISPLAY = "DISPLAY";
     public static final String TEMPERATURE_TYPE_SKIN = "SKIN";
+    private static final SensorData UNSPECIFIED_SENSOR_DATA = new SensorData(
+            /* type= */null, /* name= */ null);
 
     @Nullable
     public final String type;
@@ -43,24 +45,14 @@
     public final float maxRefreshRate;
     public final List<SupportedModeData> supportedModes;
 
-    @VisibleForTesting
-    public SensorData() {
-        this(/* type= */ null, /* name= */ null);
+    private SensorData(@Nullable String type, @Nullable String name) {
+        this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY,
+                /* supportedModes= */ List.of());
     }
 
     @VisibleForTesting
-    public SensorData(String type, String name) {
-        this(type, name, /* minRefreshRate= */ 0f, /* maxRefreshRate= */ Float.POSITIVE_INFINITY);
-    }
-
-    @VisibleForTesting
-    public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate) {
-        this(type, name, minRefreshRate, maxRefreshRate, /* supportedModes= */ List.of());
-    }
-
-    @VisibleForTesting
-    public SensorData(String type, String name, float minRefreshRate, float maxRefreshRate,
-            List<SupportedModeData> supportedModes) {
+    SensorData(@Nullable String type, @Nullable String name,
+            float minRefreshRate, float maxRefreshRate, List<SupportedModeData> supportedModes) {
         this.type = type;
         this.name = name;
         this.minRefreshRate = minRefreshRate;
@@ -72,7 +64,7 @@
      * @return True if the sensor matches both the specified name and type, or one if only one
      * is specified (not-empty). Always returns false if both parameters are null or empty.
      */
-    public boolean matches(String sensorName, String sensorType) {
+    public boolean matches(@Nullable String sensorName, @Nullable String sensorType) {
         final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
         final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
         return (isNameSpecified || isTypeSpecified)
@@ -120,7 +112,7 @@
         if (sensorDetails != null) {
             return loadSensorData(sensorDetails);
         } else {
-            return new SensorData();
+            return UNSPECIFIED_SENSOR_DATA;
         }
     }
 
@@ -130,13 +122,12 @@
     @Nullable
     public static SensorData loadProxSensorConfig(
             DisplayManagerFlags flags, DisplayConfiguration config) {
-        SensorData DEFAULT_SENSOR = new SensorData();
         List<SensorDetails> sensorDetailsList = config.getProxSensor();
         if (sensorDetailsList.isEmpty()) {
-            return DEFAULT_SENSOR;
+            return UNSPECIFIED_SENSOR_DATA;
         }
 
-        SensorData selectedSensor = DEFAULT_SENSOR;
+        SensorData selectedSensor = UNSPECIFIED_SENSOR_DATA;
         // Prioritize flagged sensors.
         for (SensorDetails sensorDetails : sensorDetailsList) {
             String flagStr = sensorDetails.getFeatureFlag();
@@ -148,7 +139,7 @@
         }
 
         // Check for normal un-flagged sensor if a flagged one wasn't found.
-        if (DEFAULT_SENSOR == selectedSensor) {
+        if (UNSPECIFIED_SENSOR_DATA == selectedSensor) {
             for (SensorDetails sensorDetails : sensorDetailsList) {
                 if (sensorDetails.getFeatureFlag() != null) {
                     continue;
@@ -159,7 +150,7 @@
         }
 
         // Check if we shouldn't use a sensor at all.
-        if (DEFAULT_SENSOR != selectedSensor) {
+        if (UNSPECIFIED_SENSOR_DATA != selectedSensor) {
             if ("".equals(selectedSensor.name) && "".equals(selectedSensor.type)) {
                 // <proxSensor> with empty values to the config means no sensor should be used.
                 // See also {@link com.android.server.display.utils.SensorUtils}
@@ -174,7 +165,7 @@
      * Loads temperature sensor data for no config case. (Type: SKIN, Name: null)
      */
     public static SensorData loadTempSensorUnspecifiedConfig() {
-        return new SensorData(TEMPERATURE_TYPE_SKIN, null);
+        return new SensorData(TEMPERATURE_TYPE_SKIN,  /* name= */ null);
     }
 
     /**
@@ -185,7 +176,7 @@
             DisplayConfiguration config) {
         SensorDetails sensorDetails = config.getTempSensor();
         if (!flags.isSensorBasedBrightnessThrottlingEnabled() || sensorDetails == null) {
-            return new SensorData(TEMPERATURE_TYPE_SKIN, null);
+            return loadTempSensorUnspecifiedConfig();
         }
         String name = sensorDetails.getName();
         String type = sensorDetails.getType();
@@ -202,7 +193,7 @@
      */
     @NonNull
     public static SensorData loadSensorUnspecifiedConfig() {
-        return new SensorData();
+        return UNSPECIFIED_SENSOR_DATA;
     }
 
     private static SensorData loadSensorData(@NonNull SensorDetails sensorDetails) {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 1f46af8..bb2efa1 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2224,12 +2224,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private void notifyConfigurationChanged(long whenNanos) {
-        mWindowManagerCallbacks.notifyConfigurationChanged();
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private void notifyInputDevicesChanged(InputDevice[] inputDevices) {
         synchronized (mInputDevicesLock) {
             if (!mInputDevicesChangedPending) {
@@ -2240,6 +2234,9 @@
 
             mInputDevices = inputDevices;
         }
+        // Input device change can possibly change configuration, so notify window manager to update
+        // its configuration.
+        mWindowManagerCallbacks.notifyConfigurationChanged();
     }
 
     // Native callback.
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
index 8ca0458..99f4747 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
@@ -20,9 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.annotation.WorkerThread;
-import android.os.Handler;
 import android.os.Process;
 import android.util.IntArray;
+import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -36,7 +36,10 @@
  * persistent storages.
  */
 final class AdditionalSubtypeMapRepository {
-    @GuardedBy("ImfLock.class")
+    private static final String TAG = "AdditionalSubtypeMapRepository";
+
+    // TODO(b/352594784): Should we user other lock primitives?
+    @GuardedBy("sPerUserMap")
     @NonNull
     private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>();
 
@@ -192,29 +195,77 @@
     private AdditionalSubtypeMapRepository() {
     }
 
+    /**
+     * Returns {@link AdditionalSubtypeMap} for the given user.
+     *
+     * <p>This method is expected be called after {@link #ensureInitializedAndGet(int)}. Otherwise
+     * {@link AdditionalSubtypeMap#EMPTY_MAP} will be returned.</p>
+     *
+     * @param userId the user to be queried about
+     * @return {@link AdditionalSubtypeMap} for the given user
+     */
+    @AnyThread
     @NonNull
-    @GuardedBy("ImfLock.class")
     static AdditionalSubtypeMap get(@UserIdInt int userId) {
-        final AdditionalSubtypeMap map = sPerUserMap.get(userId);
-        if (map != null) {
-            return map;
+        final AdditionalSubtypeMap map;
+        synchronized (sPerUserMap) {
+            map = sPerUserMap.get(userId);
         }
-        final AdditionalSubtypeMap newMap = AdditionalSubtypeUtils.load(userId);
-        sPerUserMap.put(userId, newMap);
-        return newMap;
+        if (map == null) {
+            Slog.e(TAG, "get(userId=" + userId + ") is called before loadInitialDataAndGet()."
+                    + " Returning an empty map");
+            return AdditionalSubtypeMap.EMPTY_MAP;
+        }
+        return map;
     }
 
-    @GuardedBy("ImfLock.class")
+    /**
+     * Ensures that {@link AdditionalSubtypeMap} is initialized for the given user. Load it from
+     * the persistent storage if {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} has
+     * not been called yet.
+     *
+     * @param userId the user to be initialized
+     * @return {@link AdditionalSubtypeMap} that is associated with the given user. If
+     *         {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} is already called
+     *         then the given {@link AdditionalSubtypeMap}.
+     */
+    @AnyThread
+    @NonNull
+    static AdditionalSubtypeMap ensureInitializedAndGet(@UserIdInt int userId) {
+        final var map = AdditionalSubtypeUtils.load(userId);
+        synchronized (sPerUserMap) {
+            final AdditionalSubtypeMap previous = sPerUserMap.get(userId);
+            // If putAndSave() has already been called, then use it.
+            if (previous != null) {
+                return previous;
+            }
+            sPerUserMap.put(userId, map);
+        }
+        return map;
+    }
+
+    /**
+     * Puts {@link AdditionalSubtypeMap} for the given user then schedule an I/O task to save it
+     * to the storage.
+     *
+     * @param userId         the user for the given {@link AdditionalSubtypeMap} is to be saved
+     * @param map            {@link AdditionalSubtypeMap} to be saved
+     * @param inputMethodMap {@link InputMethodMap} to be used while saving the data
+     */
+    @AnyThread
     static void putAndSave(@UserIdInt int userId, @NonNull AdditionalSubtypeMap map,
             @NonNull InputMethodMap inputMethodMap) {
-        final AdditionalSubtypeMap previous = sPerUserMap.get(userId);
-        if (previous == map) {
-            return;
+        synchronized (sPerUserMap) {
+            final AdditionalSubtypeMap previous = sPerUserMap.get(userId);
+            if (previous == map) {
+                return;
+            }
+            sPerUserMap.put(userId, map);
+            sWriter.scheduleWriteTask(userId, map, inputMethodMap);
         }
-        sPerUserMap.put(userId, map);
-        sWriter.scheduleWriteTask(userId, map, inputMethodMap);
     }
 
+    @AnyThread
     static void startWriterThread() {
         sWriter.startThread();
     }
@@ -225,12 +276,10 @@
     }
 
     @AnyThread
-    static void remove(@UserIdInt int userId, @NonNull Handler ioHandler) {
-        sWriter.onUserRemoved(userId);
-        ioHandler.post(() -> {
-            synchronized (ImfLock.class) {
-                sPerUserMap.remove(userId);
-            }
-        });
+    static void remove(@UserIdInt int userId) {
+        synchronized (sPerUserMap) {
+            sWriter.onUserRemoved(userId);
+            sPerUserMap.remove(userId);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index f61ca61..c82e5be 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -16,6 +16,8 @@
 
 package com.android.server.inputmethod;
 
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.NonNull;
@@ -110,7 +112,7 @@
             InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback cb);
 
     /**
-     * Force switch to the enabled input method by {@code imeId} for current user. If the input
+     * Force switch to the enabled input method by {@code imeId} for the current user. If the input
      * method with {@code imeId} is not enabled or not installed, do nothing.
      *
      * @param imeId  the input method ID to be switched to
@@ -119,7 +121,25 @@
      * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
      * to be switched.
      */
-    public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId);
+    public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) {
+        return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId);
+    }
+
+    /**
+     * Force switch to the enabled input method by {@code imeId} for the current user. If the input
+     * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId}
+     * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to
+     * it, otherwise the system decides the most sensible default subtype to use.
+     *
+     * @param imeId the input method ID to be switched to
+     * @param subtypeId the input method subtype ID to be switched to
+     * @param userId the user ID to be queried
+     * @return {@code true} if the current input method was successfully switched to the input
+     * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
+     * to be switched.
+     */
+    public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+            @UserIdInt int userId);
 
     /**
      * Force enable or disable the input method associated with {@code imeId} for given user. If
@@ -211,6 +231,15 @@
     public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId);
 
     /**
+     * Updates and reports whether the IME switcher button should be shown, regardless whether
+     * SystemUI or the IME is responsible for drawing it and the corresponding navigation bar.
+     *
+     * @param displayId the display for which to update the IME switcher button visibility.
+     * @param userId    the user for which to update the IME switcher button visibility.
+     */
+    public abstract void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId);
+
+    /**
      * Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if
      * there is an ongoing handwriting session.
      */
@@ -290,7 +319,8 @@
                 }
 
                 @Override
-                public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+                public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+                        @UserIdInt int userId) {
                     return false;
                 }
 
@@ -335,6 +365,10 @@
                 }
 
                 @Override
+                public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+                }
+
+                @Override
                 public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                         IAccessibilityInputMethodSession session, @UserIdInt int userId) {
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 85af7ab..a9e9dcf 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -22,8 +22,6 @@
 import static android.os.IServiceManager.DUMP_FLAG_PROTO;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
-import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE;
-import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED;
 import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION;
 import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD;
 import static android.server.inputmethod.InputMethodManagerServiceProto.CONCURRENT_MULTI_USER_MODE_ENABLED;
@@ -51,6 +49,7 @@
 
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME;
 import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
 import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
 import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO;
@@ -181,6 +180,7 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
+import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem;
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -218,6 +218,13 @@
     static final String TAG = "InputMethodManagerService";
     public static final String PROTO_ARG = "--proto";
 
+    /**
+     * Timeout in milliseconds in {@link #systemRunning()} to make sure that users are initialized
+     * in {@link Lifecycle#initializeUsersAsync(int[])}.
+     */
+    @DurationMillisLong
+    private static final long SYSTEM_READY_USER_INIT_TIMEOUT = 3000;
+
     @Retention(SOURCE)
     @IntDef({ShellCommandResult.SUCCESS, ShellCommandResult.FAILURE})
     private @interface ShellCommandResult {
@@ -360,6 +367,7 @@
     private final UserManagerInternal mUserManagerInternal;
     @MultiUserUnawareField
     private final InputMethodMenuController mMenuController;
+    private final InputMethodMenuControllerNew mMenuControllerNew;
 
     @GuardedBy("ImfLock.class")
     @MultiUserUnawareField
@@ -566,7 +574,9 @@
         }
         switch (key) {
             case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
-                mMenuController.updateKeyboardFromSettingsLocked();
+                if (!Flags.imeSwitcherRevamp()) {
+                    mMenuController.updateKeyboardFromSettingsLocked();
+                }
                 break;
             }
             case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: {
@@ -586,7 +596,7 @@
                 }
                 break;
             }
-            case STYLUS_HANDWRITING_ENABLED: {
+            case Settings.Secure.STYLUS_HANDWRITING_ENABLED: {
                 InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
                 InputMethodManager
                         .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
@@ -631,7 +641,15 @@
                         }
                     }
                 }
-                mMenuController.hideInputMethodMenu();
+                if (Flags.imeSwitcherRevamp()) {
+                    synchronized (ImfLock.class) {
+                        final var bindingController = getInputMethodBindingController(senderUserId);
+                        mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(),
+                                senderUserId);
+                    }
+                } else {
+                    mMenuController.hideInputMethodMenu();
+                }
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
             }
@@ -683,24 +701,9 @@
             super(true);
         }
 
-        @GuardedBy("ImfLock.class")
-        private boolean isChangingPackagesOfCurrentUserLocked() {
-            final int userId = getChangingUserId();
-            final boolean retval = userId == mCurrentUserId;
-            if (DEBUG) {
-                if (!retval) {
-                    Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
-                }
-            }
-            return retval;
-        }
-
         @Override
         public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
             synchronized (ImfLock.class) {
-                if (!isChangingPackagesOfCurrentUserLocked()) {
-                    return false;
-                }
                 final int userId = getChangingUserId();
                 final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
                 String curInputMethodId = settings.getSelectedInputMethod();
@@ -1023,7 +1026,7 @@
             // Called directly from UserManagerService. Do not block the calling thread.
             final int userId = user.id;
             SecureSettingsWrapper.onUserRemoved(userId);
-            AdditionalSubtypeMapRepository.remove(userId, mService.mIoHandler);
+            AdditionalSubtypeMapRepository.remove(userId);
             InputMethodSettingsRepository.remove(userId);
             mService.mUserDataRepository.remove(userId);
         }
@@ -1054,39 +1057,31 @@
 
         @AnyThread
         private void initializeUsersAsync(@UserIdInt int[] userIds) {
+            Slog.d(TAG, "Schedule initialization for users=" + Arrays.toString(userIds));
             mService.mIoHandler.post(() -> {
                 final var service = mService;
                 final var context = service.mContext;
                 final var userManagerInternal = service.mUserManagerInternal;
 
-                // We first create InputMethodMap for each user without loading AdditionalSubtypes.
-                final int numUsers = userIds.length;
-                final InputMethodMap[] rawMethodMaps = new InputMethodMap[numUsers];
-                for (int i = 0; i < numUsers; ++i) {
-                    final int userId = userIds[i];
-                    rawMethodMaps[i] = InputMethodManagerService.queryInputMethodServicesInternal(
-                            context, userId, AdditionalSubtypeMap.EMPTY_MAP,
+                for (int userId : userIds) {
+                    Slog.d(TAG, "Start initialization for user=" + userId);
+                    final var additionalSubtypeMap =
+                            AdditionalSubtypeMapRepository.ensureInitializedAndGet(userId);
+                    final var settings = InputMethodManagerService.queryInputMethodServicesInternal(
+                            context, userId, additionalSubtypeMap,
                             DirectBootAwareness.AUTO).getMethodMap();
+                    InputMethodSettingsRepository.put(userId,
+                            InputMethodSettings.create(settings, userId));
+
                     final int profileParentId = userManagerInternal.getProfileParentId(userId);
                     final boolean value =
                             InputMethodDrawsNavBarResourceMonitor.evaluate(context,
                                     profileParentId);
                     final var userData = mService.getUserData(userId);
                     userData.mImeDrawsNavBar.set(value);
-                }
 
-                // Then create full InputMethodMap for each user. Note that
-                // AdditionalSubtypeMapRepository#get() and InputMethodSettingsRepository#put()
-                // need to be called with ImfLock held (b/352387655).
-                // TODO(b/343601565): Avoid ImfLock after fixing b/352387655.
-                synchronized (ImfLock.class) {
-                    for (int i = 0; i < numUsers; ++i) {
-                        final int userId = userIds[i];
-                        final var map = AdditionalSubtypeMapRepository.get(userId);
-                        final var methodMap = rawMethodMaps[i].applyAdditionalSubtypes(map);
-                        final var settings = InputMethodSettings.create(methodMap, userId);
-                        InputMethodSettingsRepository.put(userId, settings);
-                    }
+                    userData.mBackgroundLoadLatch.countDown();
+                    Slog.d(TAG, "Complete initialization for user=" + userId);
                 }
             });
         }
@@ -1103,13 +1098,9 @@
             final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
                     userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
             InputMethodSettingsRepository.put(userId, newSettings);
-            if (mCurrentUserId == userId) {
-                // We need to rebuild IMEs.
-                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
-                updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
-            } else if (mConcurrentMultiUserModeEnabled) {
-                initializeVisibleBackgroundUserLocked(userId);
-            }
+            // We need to rebuild IMEs.
+            postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+            updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
         }
     }
 
@@ -1171,6 +1162,8 @@
                             : bindingControllerFactory);
 
             mMenuController = new InputMethodMenuController(this);
+            mMenuControllerNew = Flags.imeSwitcherRevamp()
+                    ? new InputMethodMenuControllerNew() : null;
             mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
             mVisibilityApplier = new DefaultImeVisibilityApplier(this);
 
@@ -1331,10 +1324,42 @@
         }
     }
 
+    private void waitForUserInitialization() {
+        final int[] userIds = mUserManagerInternal.getUserIds();
+        final long deadlineNanos = SystemClock.elapsedRealtimeNanos()
+                + TimeUnit.MILLISECONDS.toNanos(SYSTEM_READY_USER_INIT_TIMEOUT);
+        boolean interrupted = false;
+        try {
+            for (int userId : userIds) {
+                final var latch = getUserData(userId).mBackgroundLoadLatch;
+                boolean awaitResult;
+                while (true) {
+                    try {
+                        final long remainingNanos =
+                                Math.max(deadlineNanos - SystemClock.elapsedRealtimeNanos(), 0);
+                        awaitResult = latch.await(remainingNanos, TimeUnit.NANOSECONDS);
+                        break;
+                    } catch (InterruptedException ignored) {
+                        interrupted = true;
+                    }
+                }
+                if (!awaitResult) {
+                    Slog.w(TAG, "Timed out for user#" + userId + " to be initialized");
+                }
+            }
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
     /**
      * TODO(b/32343335): The entire systemRunning() method needs to be revisited.
      */
     public void systemRunning() {
+        waitForUserInitialization();
+
         synchronized (ImfLock.class) {
             if (DEBUG) {
                 Slog.d(TAG, "--- systemReady");
@@ -1381,16 +1406,13 @@
                 final String defaultImiId = SecureSettingsWrapper.getString(
                         Settings.Secure.DEFAULT_INPUT_METHOD, null, currentUserId);
                 final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId);
-                final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
-                        currentUserId, AdditionalSubtypeMapRepository.get(currentUserId),
-                        DirectBootAwareness.AUTO);
-                InputMethodSettingsRepository.put(currentUserId, newSettings);
+                final var settings = InputMethodSettingsRepository.get(currentUserId);
                 postInputMethodSettingUpdatedLocked(
                         !imeSelectedOnBoot /* resetDefaultEnabledIme */, currentUserId);
                 updateFromSettingsLocked(true, currentUserId);
                 InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                         getPackageManagerForUser(mContext, currentUserId),
-                        newSettings.getEnabledInputMethodList());
+                        settings.getEnabledInputMethodList());
 
                 AdditionalSubtypeMapRepository.startWriterThread();
 
@@ -1410,9 +1432,9 @@
                 (windowToken, imeVisible) -> {
                     if (Flags.refactorInsetsController()) {
                         if (imeVisible) {
-                            showSoftInputInternal(windowToken);
+                            showCurrentInputInternal(windowToken);
                         } else {
-                            hideSoftInputInternal(windowToken);
+                            hideCurrentInputInternal(windowToken);
                         }
                     }
                 });
@@ -1586,8 +1608,8 @@
         // If user is a profile, use preference of it`s parent profile.
         final int profileParentUserId = mUserManagerInternal.getProfileParentId(userId);
         if (Settings.Secure.getIntForUser(context.getContentResolver(),
-                STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE,
-                profileParentUserId) == 0) {
+                Settings.Secure.STYLUS_HANDWRITING_ENABLED,
+                Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE, profileParentUserId) == 0) {
             return false;
         }
         return true;
@@ -1782,7 +1804,11 @@
                     ImeTracker.PHASE_SERVER_WAIT_IME);
             userData.mCurStatsToken = null;
             // TODO: Make mMenuController multi-user aware
-            mMenuController.hideInputMethodMenuLocked();
+            if (Flags.imeSwitcherRevamp()) {
+                mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+            } else {
+                mMenuController.hideInputMethodMenuLocked();
+            }
         }
     }
 
@@ -1884,7 +1910,7 @@
         if (Flags.refactorInsetsController()) {
             if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null
                     && userData.mImeBindingState.mFocusedWindow != null) {
-                showSoftInputInternal(userData.mImeBindingState.mFocusedWindow);
+                showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow);
             }
         } else {
             if (isShowRequestedForCurrentWindow(userId)) {
@@ -2599,7 +2625,12 @@
         if (!mShowOngoingImeSwitcherForPhones) return false;
         // When the IME switcher dialog is shown, the IME switcher button should be hidden.
         // TODO(b/305849394): Make mMenuController multi-user aware.
-        if (mMenuController.getSwitchingDialogLocked() != null) return false;
+        final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+                ? mMenuControllerNew.isShowing()
+                : mMenuController.getSwitchingDialogLocked() != null;
+        if (switcherMenuShowing) {
+            return false;
+        }
         // When we are switching IMEs, the IME switcher button should be hidden.
         final var bindingController = getInputMethodBindingController(userId);
         if (!Objects.equals(bindingController.getCurId(),
@@ -2614,7 +2645,7 @@
                 || (visibility & InputMethodService.IME_INVISIBLE) != 0) {
             return false;
         }
-        if (mWindowManagerInternal.isHardKeyboardAvailable()) {
+        if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) {
             // When physical keyboard is attached, we show the ime switcher (or notification if
             // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently
             // exists in the IME switcher dialog.  Might be OK to remove this condition once
@@ -2625,6 +2656,15 @@
         }
 
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+        if (Flags.imeSwitcherRevamp()) {
+            // The IME switcher button should be shown when the current IME specified a
+            // language settings activity.
+            final var curImi = settings.getMethodMap().get(settings.getSelectedInputMethod());
+            if (curImi != null && curImi.createImeLanguageSettingsActivityIntent() != null) {
+                return true;
+            }
+        }
+
         return hasMultipleSubtypesForSwitcher(false /* nonAuxOnly */, settings);
     }
 
@@ -2794,7 +2834,10 @@
             }
             final var curId = bindingController.getCurId();
             // TODO(b/305849394): Make mMenuController multi-user aware.
-            if (mMenuController.getSwitchingDialogLocked() != null
+            final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+                    ? mMenuControllerNew.isShowing()
+                    : mMenuController.getSwitchingDialogLocked() != null;
+            if (switcherMenuShowing
                     || !Objects.equals(curId, bindingController.getSelectedMethodId())) {
                 // When the IME switcher dialog is shown, or we are switching IMEs,
                 // the back button should be in the default state (as if the IME is not shown).
@@ -2813,7 +2856,9 @@
     @GuardedBy("ImfLock.class")
     void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
         updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
-        mMenuController.updateKeyboardFromSettingsLocked();
+        if (!Flags.imeSwitcherRevamp()) {
+            mMenuController.updateKeyboardFromSettingsLocked();
+        }
     }
 
     /**
@@ -3097,8 +3142,8 @@
         }
     }
 
-    boolean showSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal");
+    boolean showCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput", mDumper);
         synchronized (ImfLock.class) {
@@ -3117,8 +3162,8 @@
         }
     }
 
-    boolean hideSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal");
+    boolean hideCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput", mDumper);
         synchronized (ImfLock.class) {
@@ -3979,10 +4024,70 @@
     @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
     public boolean isInputMethodPickerShownForTest() {
         synchronized (ImfLock.class) {
-            return mMenuController.isisInputMethodPickerShownForTestLocked();
+            return Flags.imeSwitcherRevamp()
+                    ? mMenuControllerNew.isShowing()
+                    : mMenuController.isisInputMethodPickerShownForTestLocked();
         }
     }
 
+    /**
+     * Gets the list of Input Method Switcher Menu items and the index of the selected item.
+     *
+     * @param items             the list of input method and subtype items.
+     * @param selectedImeId     the ID of the selected input method.
+     * @param selectedSubtypeId the ID of the selected input method subtype,
+     *                          or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected.
+     * @param userId            the ID of the user for which to get the menu items.
+     * @return the list of menu items, and the index of the selected item,
+     * or {@code -1} if no item is selected.
+     */
+    @GuardedBy("ImfLock.class")
+    @NonNull
+    private Pair<List<MenuItem>, Integer> getInputMethodPickerItems(
+            @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId,
+            int selectedSubtypeId, @UserIdInt int userId) {
+        final var bindingController = getInputMethodBindingController(userId);
+        final var settings = InputMethodSettingsRepository.get(userId);
+
+        if (selectedSubtypeId == NOT_A_SUBTYPE_ID) {
+            // TODO(b/351124299): Check if this fallback logic is still necessary.
+            final var curSubtype = bindingController.getCurrentInputMethodSubtype();
+            if (curSubtype != null) {
+                final var curMethodId = bindingController.getSelectedMethodId();
+                final var curImi = settings.getMethodMap().get(curMethodId);
+                selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
+                        curImi, curSubtype.hashCode());
+            }
+        }
+
+        // No item is selected by default. When we have a list of explicitly enabled
+        // subtypes, the implicit subtype is no longer listed. If the implicit one
+        // is still selected, no items will be shown as selected.
+        int selectedIndex = -1;
+        String prevImeId = null;
+        final var menuItems = new ArrayList<MenuItem>();
+        for (int i = 0; i < items.size(); i++) {
+            final var item = items.get(i);
+            final var imeId = item.mImi.getId();
+            if (imeId.equals(selectedImeId)) {
+                final int subtypeId = item.mSubtypeId;
+                // Check if this is the selected IME-subtype pair.
+                if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID)
+                        || subtypeId == NOT_A_SUBTYPE_ID
+                        || subtypeId == selectedSubtypeId) {
+                    selectedIndex = i;
+                }
+            }
+            final boolean hasHeader = !imeId.equals(prevImeId);
+            final boolean hasDivider = hasHeader && prevImeId != null;
+            prevImeId = imeId;
+            menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId,
+                    hasHeader, hasDivider));
+        }
+
+        return new Pair<>(menuItems, selectedIndex);
+    }
+
     @BinderThread
     private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
             @NonNull UserData userData) {
@@ -4625,7 +4730,10 @@
             proto.write(IS_INTERACTIVE, mIsInteractive);
             proto.write(BACK_DISPOSITION, bindingController.getBackDisposition());
             proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis());
-            proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard());
+            if (!Flags.imeSwitcherRevamp()) {
+                proto.write(SHOW_IME_WITH_HARD_KEYBOARD,
+                        mMenuController.getShowImeWithHardKeyboard());
+            }
             proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled);
             proto.end(token);
         }
@@ -4670,8 +4778,7 @@
                 final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(
                         windowToken, userId);
                 mVisibilityApplier.applyImeVisibility(requestToken, statsToken,
-                        setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME
-                                : ImeVisibilityStateComputer.STATE_HIDE_IME,
+                        setVisible ? STATE_SHOW_IME : STATE_HIDE_IME,
                         SoftInputShowHideReason.NOT_SET /* ignore reason */, userId);
             }
         } finally {
@@ -4931,8 +5038,9 @@
                 synchronized (ImfLock.class) {
                     final InputMethodSettings settings =
                             InputMethodSettingsRepository.get(mCurrentUserId);
+                    final int userId = settings.getUserId();
                     final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
-                            && mWindowManagerInternal.isKeyguardSecure(settings.getUserId());
+                            && mWindowManagerInternal.isKeyguardSecure(userId);
                     final String lastInputMethodId = settings.getSelectedInputMethod();
                     int lastInputMethodSubtypeId =
                             settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
@@ -4945,12 +5053,35 @@
                         Slog.w(TAG, "Show switching menu failed, imList is empty,"
                                 + " showAuxSubtypes: " + showAuxSubtypes
                                 + " isScreenLocked: " + isScreenLocked
-                                + " userId: " + settings.getUserId());
+                                + " userId: " + userId);
                         return false;
                     }
 
-                    mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
-                            lastInputMethodId, lastInputMethodSubtypeId, imList);
+                    if (Flags.imeSwitcherRevamp()) {
+                        if (DEBUG) {
+                            Slog.v(TAG, "Show IME switcher menu,"
+                                    + " showAuxSubtypes=" + showAuxSubtypes
+                                    + " displayId=" + displayId
+                                    + " preferredInputMethodId=" + lastInputMethodId
+                                    + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
+                        }
+
+                        final var itemsAndIndex = getInputMethodPickerItems(imList,
+                                lastInputMethodId, lastInputMethodSubtypeId, userId);
+                        final var menuItems = itemsAndIndex.first;
+                        final int selectedIndex = itemsAndIndex.second;
+
+                        if (selectedIndex == -1) {
+                            Slog.w(TAG, "Switching menu shown with no item selected"
+                                    + ", IME id: " + lastInputMethodId
+                                    + ", subtype index: " + lastInputMethodSubtypeId);
+                        }
+
+                        mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+                    } else {
+                        mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+                                lastInputMethodId, lastInputMethodSubtypeId, imList);
+                    }
                 }
                 return true;
 
@@ -5021,7 +5152,9 @@
 
             // --------------------------------------------------------------
             case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
-                mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+                if (!Flags.imeSwitcherRevamp()) {
+                    mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+                }
                 synchronized (ImfLock.class) {
                     sendOnNavButtonFlagsChangedToAllImesLocked();
                 }
@@ -5591,7 +5724,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
+    private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId,
+            @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) {
             if (!settings.getMethodMap().containsKey(imeId)
@@ -5599,7 +5733,7 @@
                     .contains(settings.getMethodMap().get(imeId))) {
                 return false; // IME is not found or not enabled.
             }
-            setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId);
+            setInputMethodLocked(imeId, subtypeId, userId);
             return true;
         }
         if (!settings.getMethodMap().containsKey(imeId)
@@ -5608,6 +5742,7 @@
             return false; // IME is not found or not enabled.
         }
         settings.putSelectedInputMethod(imeId);
+        // For non-current user, only reset subtypeId (instead of setting the given one).
         settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
         return true;
     }
@@ -5753,9 +5888,10 @@
         }
 
         @Override
-        public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+        public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+                @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                return switchToInputMethodLocked(imeId, userId);
+                return switchToInputMethodLocked(imeId, subtypeId, userId);
             }
         }
 
@@ -5852,7 +5988,12 @@
                 // input target changed, in case seeing the dialog dismiss flickering during
                 // the next focused window starting the input connection.
                 if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) {
-                    mMenuController.hideInputMethodMenuLocked();
+                    if (Flags.imeSwitcherRevamp()) {
+                        final var bindingController = getInputMethodBindingController(userId);
+                        mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+                    } else {
+                        mMenuController.hideInputMethodMenuLocked();
+                    }
                 }
             }
         }
@@ -5871,6 +6012,15 @@
         }
 
         @Override
+        public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+            synchronized (ImfLock.class) {
+                updateSystemUiLocked(userId);
+                final var userData = getUserData(userId);
+                sendOnNavButtonFlagsChangedLocked(userData);
+            }
+        }
+
+        @Override
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
@@ -6114,7 +6264,6 @@
             boolean isCritical) {
         IInputMethodInvoker method;
         ClientState client;
-        ClientState focusedWindowClient;
 
         final Printer p = new PrintWriterPrinter(pw);
 
@@ -6192,6 +6341,10 @@
                     };
             mUserDataRepository.forAllUserData(userDataDump);
 
+            if (Flags.imeSwitcherRevamp()) {
+                p.println("  menuControllerNew:");
+                mMenuControllerNew.dump(p, "  ");
+            }
             p.println("  mCurToken=" + bindingController.getCurToken());
             p.println("  mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
             p.println("  mCurHostInputToken=" + bindingController.getCurHostInputToken());
@@ -6638,7 +6791,7 @@
                         continue;
                     }
                     boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId,
-                            userId);
+                            NOT_A_SUBTYPE_ID, userId);
                     if (failedToSelectUnknownIme) {
                         error.print("Unknown input method ");
                         error.print(imeId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
new file mode 100644
index 0000000..045414b
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -0,0 +1,356 @@
+/*
+ * 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.inputmethod;
+
+
+import static android.Manifest.permission.HIDE_OVERLAY_WINDOWS;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+
+import static com.android.server.inputmethod.InputMethodManagerService.DEBUG;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodInfo;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Controller for showing and hiding the Input Method Switcher Menu.
+ */
+final class InputMethodMenuControllerNew {
+
+    private static final String TAG = InputMethodMenuControllerNew.class.getSimpleName();
+
+    /**
+     * The horizontal offset from the menu to the edge of the screen corresponding
+     * to {@link Gravity#END}.
+     */
+    private static final int HORIZONTAL_OFFSET = 16;
+
+    /** The title of the window, used for debugging. */
+    private static final String WINDOW_TITLE = "IME Switcher Menu";
+
+    private final InputMethodDialogWindowContext mDialogWindowContext =
+            new InputMethodDialogWindowContext();
+
+    @Nullable
+    private AlertDialog mDialog;
+
+    @Nullable
+    private List<MenuItem> mMenuItems;
+
+    /**
+     * Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes.
+     *
+     * @param items         the list of menu items.
+     * @param selectedIndex the index of the menu item that is selected.
+     *                      If no other IMEs are enabled, this index will be out of reach.
+     * @param displayId     the ID of the display where the menu was requested.
+     * @param userId        the ID of the user that requested the menu.
+     */
+    @RequiresPermission(allOf = {INTERACT_ACROSS_USERS, HIDE_OVERLAY_WINDOWS})
+    void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId,
+            @UserIdInt int userId) {
+        // Hide the menu in case it was already showing.
+        hide(displayId, userId);
+
+        final Context dialogWindowContext = mDialogWindowContext.get(displayId);
+        final var builder = new AlertDialog.Builder(dialogWindowContext,
+                com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog);
+        final var inflater = LayoutInflater.from(builder.getContext());
+
+        // Create the content view.
+        final View contentView = inflater
+                .inflate(com.android.internal.R.layout.input_method_switch_dialog_new, null);
+        contentView.setAccessibilityPaneTitle(
+                dialogWindowContext.getText(com.android.internal.R.string.select_input_method));
+        builder.setView(contentView);
+
+        final DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
+            if (which != selectedIndex) {
+                final var item = items.get(which);
+                InputMethodManagerInternal.get()
+                        .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId);
+            }
+            hide(displayId, userId);
+        };
+
+        final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null;
+        final var languageSettingsIntent = selectedImi != null
+                ? selectedImi.createImeLanguageSettingsActivityIntent() : null;
+        final boolean hasLanguageSettingsButton = languageSettingsIntent != null;
+        if (hasLanguageSettingsButton) {
+            final View buttonBar = contentView
+                    .requireViewById(com.android.internal.R.id.button_bar);
+            buttonBar.setVisibility(View.VISIBLE);
+
+            languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            final Button languageSettingsButton = contentView
+                    .requireViewById(com.android.internal.R.id.button1);
+            languageSettingsButton.setVisibility(View.VISIBLE);
+            languageSettingsButton.setOnClickListener(v -> {
+                v.getContext().startActivityAsUser(languageSettingsIntent, UserHandle.of(userId));
+                hide(displayId, userId);
+            });
+        }
+
+        // Create the current IME subtypes list.
+        final RecyclerView recyclerView = contentView
+                .requireViewById(com.android.internal.R.id.list);
+        recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener));
+        // Scroll to the currently selected IME.
+        recyclerView.scrollToPosition(selectedIndex);
+        // Indicate that the list can be scrolled.
+        recyclerView.setScrollIndicators(
+                hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0);
+
+        builder.setOnCancelListener(dialog -> hide(displayId, userId));
+        mMenuItems = items;
+        mDialog = builder.create();
+        mDialog.setCanceledOnTouchOutside(true);
+        final Window w = mDialog.getWindow();
+        w.setHideOverlayWindows(true);
+        final WindowManager.LayoutParams attrs = w.getAttributes();
+        // Use an alternate token for the dialog for that window manager can group the token
+        // with other IME windows based on type vs. grouping based on whichever token happens
+        // to get selected by the system later on.
+        attrs.token = dialogWindowContext.getWindowContextToken();
+        attrs.gravity = Gravity.getAbsoluteGravity(Gravity.BOTTOM | Gravity.END,
+                dialogWindowContext.getResources().getConfiguration().getLayoutDirection());
+        attrs.x = HORIZONTAL_OFFSET;
+        attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+        attrs.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+        // Used for debugging only, not user visible.
+        attrs.setTitle(WINDOW_TITLE);
+        w.setAttributes(attrs);
+
+        mDialog.show();
+        InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+    }
+
+    /**
+     * Hides the Input Method Switcher Menu.
+     *
+     * @param displayId the ID of the display from where the menu should be hidden.
+     * @param userId    the ID of the user for which the menu should be hidden.
+     */
+    void hide(int displayId, @UserIdInt int userId) {
+        if (DEBUG) Slog.v(TAG, "Hide IME switcher menu.");
+
+        mMenuItems = null;
+        // Cannot use dialog.isShowing() here, as the cancel listener flow already resets mShowing.
+        if (mDialog != null) {
+            mDialog.dismiss();
+            mDialog = null;
+
+            InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+        }
+    }
+
+    /**
+     * Returns whether the Input Method Switcher Menu is showing.
+     */
+    boolean isShowing() {
+        return mDialog != null && mDialog.isShowing();
+    }
+
+    void dump(@NonNull Printer pw, @NonNull String prefix) {
+        final boolean showing = isShowing();
+        pw.println(prefix + "  isShowing: " + showing);
+
+        if (showing) {
+            pw.println(prefix + "  menuItems: " + mMenuItems);
+        }
+    }
+
+    /**
+     * Item to be shown in the Input Method Switcher Menu, containing an input method and
+     * optionally an input method subtype.
+     */
+    static class MenuItem {
+
+        /** The name of the input method. */
+        @NonNull
+        private final CharSequence mImeName;
+
+        /**
+         * The name of the input method subtype, or {@code null} if this item doesn't have a
+         * subtype.
+         */
+        @Nullable
+        private final CharSequence mSubtypeName;
+
+        /** The info of the input method. */
+        @NonNull
+        private final InputMethodInfo mImi;
+
+        /**
+         * The index of the subtype in the input method's array of subtypes,
+         * or {@link InputMethodUtils#NOT_A_SUBTYPE_ID} if this item doesn't have a subtype.
+         */
+        @IntRange(from = NOT_A_SUBTYPE_ID)
+        private final int mSubtypeId;
+
+        /** Whether this item has a group header (only the first item of each input method). */
+        private final boolean mHasHeader;
+
+        /**
+         * Whether this item should has a group divider (same as {@link #mHasHeader},
+         * excluding the first IME).
+         */
+        private final boolean mHasDivider;
+
+        MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName,
+                @NonNull InputMethodInfo imi, @IntRange(from = NOT_A_SUBTYPE_ID) int subtypeId,
+                boolean hasHeader, boolean hasDivider) {
+            mImeName = imeName;
+            mSubtypeName = subtypeName;
+            mImi = imi;
+            mSubtypeId = subtypeId;
+            mHasHeader = hasHeader;
+            mHasDivider = hasDivider;
+        }
+
+        @Override
+        public String toString() {
+            return "MenuItem{"
+                    + "mImeName=" + mImeName
+                    + " mSubtypeName=" + mSubtypeName
+                    + " mSubtypeId=" + mSubtypeId
+                    + " mHasHeader=" + mHasHeader
+                    + " mHasDivider=" + mHasDivider
+                    + "}";
+        }
+    }
+
+    private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
+
+        /** The list of items to show. */
+        @NonNull
+        private final List<MenuItem> mItems;
+        /** The index of the selected item. */
+        private final int mSelectedIndex;
+        @NonNull
+        private final LayoutInflater mInflater;
+        @NonNull
+        private final DialogInterface.OnClickListener mOnClickListener;
+
+        Adapter(@NonNull List<MenuItem> items, int selectedIndex,
+                @NonNull LayoutInflater inflater,
+                @NonNull DialogInterface.OnClickListener onClickListener) {
+            mItems = items;
+            mSelectedIndex = selectedIndex;
+            mInflater = inflater;
+            mOnClickListener = onClickListener;
+        }
+
+        @Override
+        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            final View view = mInflater.inflate(
+                    com.android.internal.R.layout.input_method_switch_item_new, parent, false);
+
+            return new ViewHolder(view, mOnClickListener);
+        }
+
+        @Override
+        public void onBindViewHolder(ViewHolder holder, int position) {
+            holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mItems.size();
+        }
+
+        private static class ViewHolder extends RecyclerView.ViewHolder {
+
+            /** The container of the item. */
+            @NonNull
+            private final View mContainer;
+            /** The name of the item. */
+            @NonNull
+            private final TextView mName;
+            /** Indicator for the selected status of the item. */
+            @NonNull
+            private final ImageView mCheckmark;
+            /** The group header optionally drawn above the item. */
+            @NonNull
+            private final TextView mHeader;
+            /** The group divider optionally drawn above the item. */
+            @NonNull
+            private final View mDivider;
+
+            private ViewHolder(@NonNull View itemView,
+                    @NonNull DialogInterface.OnClickListener onClickListener) {
+                super(itemView);
+
+                mContainer = itemView.requireViewById(com.android.internal.R.id.list_item);
+                mName = itemView.requireViewById(com.android.internal.R.id.text);
+                mCheckmark = itemView.requireViewById(com.android.internal.R.id.image);
+                mHeader = itemView.requireViewById(com.android.internal.R.id.header_text);
+                mDivider = itemView.requireViewById(com.android.internal.R.id.divider);
+
+                mContainer.setOnClickListener((v) ->
+                        onClickListener.onClick(null /* dialog */, getAdapterPosition()));
+            }
+
+            /**
+             * Binds the given item to the current view.
+             *
+             * @param item       the item to bind.
+             * @param isSelected whether this is selected.
+             */
+            private void bind(@NonNull MenuItem item, boolean isSelected) {
+                // Use the IME name for subtypes with an empty subtype name.
+                final var name = TextUtils.isEmpty(item.mSubtypeName)
+                        ? item.mImeName : item.mSubtypeName;
+                mContainer.setActivated(isSelected);
+                // Activated is the correct state, but we also set selected for accessibility info.
+                mContainer.setSelected(isSelected);
+                mName.setSelected(isSelected);
+                mName.setText(name);
+                mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+                mHeader.setText(item.mImeName);
+                mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE);
+                mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE);
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
index 50ba364..1b84036 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java
@@ -24,7 +24,8 @@
 import com.android.internal.annotations.GuardedBy;
 
 final class InputMethodSettingsRepository {
-    @GuardedBy("ImfLock.class")
+    // TODO(b/352594784): Should we user other lock primitives?
+    @GuardedBy("sPerUserMap")
     @NonNull
     private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>();
 
@@ -35,23 +36,28 @@
     }
 
     @NonNull
-    @GuardedBy("ImfLock.class")
+    @AnyThread
     static InputMethodSettings get(@UserIdInt int userId) {
-        final InputMethodSettings obj = sPerUserMap.get(userId);
+        final InputMethodSettings obj;
+        synchronized (sPerUserMap) {
+            obj = sPerUserMap.get(userId);
+        }
         if (obj != null) {
             return obj;
         }
         return InputMethodSettings.createEmptyMap(userId);
     }
 
-    @GuardedBy("ImfLock.class")
+    @AnyThread
     static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) {
-        sPerUserMap.put(userId, obj);
+        synchronized (sPerUserMap) {
+            sPerUserMap.put(userId, obj);
+        }
     }
 
     @AnyThread
     static void remove(@UserIdInt int userId) {
-        synchronized (ImfLock.class) {
+        synchronized (sPerUserMap) {
             sPerUserMap.remove(userId);
         }
     }
diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java
index ec5c9e6..be57321 100644
--- a/services/core/java/com/android/server/inputmethod/UserData.java
+++ b/services/core/java/com/android/server/inputmethod/UserData.java
@@ -28,6 +28,7 @@
 import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
 import com.android.internal.inputmethod.IRemoteInputConnection;
 
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Placeholder for all IMMS user specific fields */
@@ -35,6 +36,13 @@
     @UserIdInt
     final int mUserId;
 
+    /**
+     * Tells whether {@link InputMethodManagerService.Lifecycle#initializeUsersAsync(int[])} is
+     * completed for this user or not.
+     */
+    @NonNull
+    final CountDownLatch mBackgroundLoadLatch = new CountDownLatch(1);
+
     @NonNull
     final InputMethodBindingController mBindingController;
 
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 6e991b4..2e167ef 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -376,6 +376,11 @@
                             mContext.getContentResolver(),
                             Settings.Global.LOCATION_ENABLE_STATIONARY_THROTTLE,
                             defaultStationaryThrottlingSetting) != 0;
+                    if (Flags.disableStationaryThrottling() && !(
+                            Flags.keepGnssStationaryThrottling() && enableStationaryThrottling
+                                    && GPS_PROVIDER.equals(manager.getName()))) {
+                        enableStationaryThrottling = false;
+                    }
                     if (enableStationaryThrottling) {
                         realProvider = new StationaryThrottlingLocationProvider(manager.getName(),
                                 mInjector, realProvider);
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 ed451ff..7de1045 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -333,8 +333,14 @@
         return new IContextHubClientCallback.Stub() {
             private void finishCallback() {
                 try {
-                    IContextHubClient client = mDefaultClientMap.get(contextHubId);
-                    client.callbackFinished();
+                    if (mDefaultClientMap != null && mDefaultClientMap.containsKey(contextHubId)) {
+                        IContextHubClient client = mDefaultClientMap.get(contextHubId);
+                        client.callbackFinished();
+                    } else {
+                        Log.e(TAG, "Default client not found for hub (ID = " + contextHubId + "): "
+                                + mDefaultClientMap == null ? "map was null"
+                                                            : "map did not contain the hub");
+                    }
                 } catch (RemoteException e) {
                     Log.e(
                             TAG,
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
index 17f2fcc..bb35b37 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
@@ -340,6 +340,11 @@
                 getOutPrintWriter().println("Profile uses unified challenge");
                 return false;
             }
+            if (mOld.isEmpty()) {
+                getOutPrintWriter().println(
+                        "User has a lock credential, but old credential was not provided");
+                return false;
+            }
 
             try {
                 final boolean result = mLockPatternUtils.checkCredential(getOldCredential(),
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index ba7d3b8..d9f3622 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -251,6 +251,10 @@
                         systemRoutes = providerInfo.getRoutes();
                     } else {
                         systemRoutes = Collections.emptyList();
+                        Slog.e(
+                                TAG,
+                                "Returning empty system routes list because "
+                                    + "system provider has null providerInfo.");
                     }
                 } else {
                     systemRoutes = new ArrayList<>();
diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java
index 5ea3e70..74f0d9c 100644
--- a/services/core/java/com/android/server/net/NetworkManagementService.java
+++ b/services/core/java/com/android/server/net/NetworkManagementService.java
@@ -81,8 +81,6 @@
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.HexDump;
-import com.android.modules.utils.build.SdkLevel;
-import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
@@ -833,144 +831,6 @@
     }
 
     @Override
-    public boolean getIpForwardingEnabled() throws IllegalStateException{
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException(
-                    "NMS#getIpForwardingEnabled not supported in V+");
-        }
-        try {
-            return mNetdService.ipfwdEnabled();
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void setIpForwardingEnabled(boolean enable) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException(
-                    "NMS#setIpForwardingEnabled not supported in V+");
-        }        try {
-            if (enable) {
-                mNetdService.ipfwdEnableForwarding("tethering");
-            } else {
-                mNetdService.ipfwdDisableForwarding("tethering");
-            }
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void startTethering(String[] dhcpRange) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#startTethering not supported in V+");
-        }
-        try {
-            NetdUtils.tetherStart(mNetdService, true /* usingLegacyDnsProxy */, dhcpRange);
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void stopTethering() {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#stopTethering not supported in V+");
-        }
-        try {
-            mNetdService.tetherStop();
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public boolean isTetheringStarted() {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#isTetheringStarted not supported in V+");
-        }
-        try {
-            return mNetdService.tetherIsEnabled();
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void tetherInterface(String iface) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#tetherInterface not supported in V+");
-        }
-        try {
-            final LinkAddress addr = getInterfaceConfig(iface).getLinkAddress();
-            final IpPrefix dest = new IpPrefix(addr.getAddress(), addr.getPrefixLength());
-            NetdUtils.tetherInterface(mNetdService, iface, dest);
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void untetherInterface(String iface) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#untetherInterface not supported in V+");
-        }
-        try {
-            NetdUtils.untetherInterface(mNetdService, iface);
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public String[] listTetheredInterfaces() {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException(
-                    "NMS#listTetheredInterfaces not supported in V+");
-        }
-        try {
-            return mNetdService.tetherInterfaceList();
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void enableNat(String internalInterface, String externalInterface) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#enableNat not supported in V+");
-        }
-        try {
-            mNetdService.tetherAddForward(internalInterface, externalInterface);
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void disableNat(String internalInterface, String externalInterface) {
-        PermissionUtils.enforceNetworkStackPermission(mContext);
-        if (SdkLevel.isAtLeastV()) {
-            throw new UnsupportedOperationException("NMS#disableNat not supported in V+");
-        }
-        try {
-            mNetdService.tetherRemoveForward(internalInterface, externalInterface);
-        } catch (RemoteException | ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
     public void setInterfaceQuota(String iface, long quotaBytes) {
         PermissionUtils.enforceNetworkStackPermission(mContext);
 
@@ -1126,30 +986,19 @@
             }
             Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "setDataSaverModeEnabled");
             try {
-                if (SdkLevel.isAtLeastV()) {
-                    // setDataSaverEnabled throws if it fails to set data saver.
-                    mContext.getSystemService(ConnectivityManager.class)
-                            .setDataSaverEnabled(enable);
-                    mDataSaverMode = enable;
-                    if (mUseMeteredFirewallChains) {
-                        // Copy mDataSaverMode state to FIREWALL_CHAIN_METERED_ALLOW
-                        // until ConnectivityService allows manipulation of the data saver mode via
-                        // FIREWALL_CHAIN_METERED_ALLOW.
-                        synchronized (mRulesLock) {
-                            mFirewallChainStates.put(FIREWALL_CHAIN_METERED_ALLOW, enable);
-                        }
+                // setDataSaverEnabled throws if it fails to set data saver.
+                mContext.getSystemService(ConnectivityManager.class).setDataSaverEnabled(enable);
+                mDataSaverMode = enable;
+                if (mUseMeteredFirewallChains) {
+                    // Copy mDataSaverMode state to FIREWALL_CHAIN_METERED_ALLOW
+                    // until ConnectivityService allows manipulation of the data saver mode via
+                    // FIREWALL_CHAIN_METERED_ALLOW.
+                    synchronized (mRulesLock) {
+                        mFirewallChainStates.put(FIREWALL_CHAIN_METERED_ALLOW, enable);
                     }
-                    return true;
-                } else {
-                    final boolean changed = mNetdService.bandwidthEnableDataSaver(enable);
-                    if (changed) {
-                        mDataSaverMode = enable;
-                    } else {
-                        Log.e(TAG, "setDataSaverMode(" + enable + "): failed to set iptables");
-                    }
-                    return changed;
                 }
-            } catch (RemoteException | IllegalStateException e) {
+                return true;
+            } catch (IllegalStateException e) {
                 Log.e(TAG, "setDataSaverMode(" + enable + "): failed with exception", e);
                 return false;
             } finally {
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index d9e22c5..53b6796 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4118,7 +4118,7 @@
                 fout.increaseIndent();
                 for (int i = 0; i < mSubscriptionPlans.size(); i++) {
                     final int subId = mSubscriptionPlans.keyAt(i);
-                    fout.println("Subscriber ID " + subId + ":");
+                    fout.println("Subscription ID " + subId + ":");
                     fout.increaseIndent();
                     final SubscriptionPlan[] plans = mSubscriptionPlans.valueAt(i);
                     if (!ArrayUtils.isEmpty(plans)) {
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index c078409..b12a917 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -1159,11 +1159,18 @@
                 rule.conditionId = azr.getConditionId();
                 modified = true;
             }
-            boolean shouldPreserveCondition = Flags.modesApi() && Flags.modesUi()
-                    && !isNew && origin == UPDATE_ORIGIN_USER
-                    && rule.enabled == azr.isEnabled()
-                    && rule.conditionId != null && rule.condition != null
-                    && rule.conditionId.equals(rule.condition.id);
+            // This can be removed when {@link Flags#modesUi} is fully ramped up
+            final boolean isWatch =
+                    mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+            boolean shouldPreserveCondition =
+                    Flags.modesApi()
+                            && (Flags.modesUi() || isWatch)
+                            && !isNew
+                            && origin == UPDATE_ORIGIN_USER
+                            && rule.enabled == azr.isEnabled()
+                            && rule.conditionId != null
+                            && rule.condition != null
+                            && rule.conditionId.equals(rule.condition.id);
             if (!shouldPreserveCondition) {
                 // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR.
                 rule.condition = null;
diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
index 58b14b1..15e758c 100644
--- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
+++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java
@@ -37,7 +37,6 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.util.Log;
 
 import com.android.internal.R;
@@ -47,9 +46,9 @@
 
     private static final boolean DEBUG = false;
     private static final String LOG_TAG = BackgroundUserSoundNotifier.class.getSimpleName();
-    public static final String BUSN_CHANNEL_ID = "bg_user_sound_channel";
-    public static final String BUSN_CHANNEL_NAME = "BackgroundUserSound";
-    private static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER";
+    private static final String BUSN_CHANNEL_ID = "bg_user_sound_channel";
+    private static final String BUSN_CHANNEL_NAME = "BackgroundUserSound";
+    public static final String ACTION_MUTE_SOUND = "com.android.server.ACTION_MUTE_BG_USER";
     private static final String EXTRA_NOTIFICATION_ID = "com.android.server.EXTRA_CLIENT_UID";
     private static final String EXTRA_CURRENT_USER_ID = "com.android.server.EXTRA_CURRENT_USER_ID";
     private static final String ACTION_SWITCH_USER = "com.android.server.ACTION_SWITCH_TO_USER";
@@ -144,6 +143,7 @@
                                     -1) + "  current user id " + intent.getIntExtra(
                                     EXTRA_CURRENT_USER_ID, -1));
                 }
+                mUserWithNotification = -1;
                 mNotificationManager.cancelAsUser(LOG_TAG, notificationId,
                         UserHandle.of(intent.getIntExtra(EXTRA_CURRENT_USER_ID, -1)));
                 if (ACTION_MUTE_SOUND.equals(intent.getAction())) {
@@ -159,10 +159,6 @@
                             }
                         }
                     }
-                    Vibrator vibrator = mSystemUserContext.getSystemService(Vibrator.class);
-                    if (vibrator != null && vibrator.isVibrating()) {
-                        vibrator.cancel();
-                    }
                 } else if (ACTION_SWITCH_USER.equals(intent.getAction())) {
                     service.switchUser(intent.getIntExtra(Intent.EXTRA_USER_ID, -1));
                 }
diff --git a/services/core/java/com/android/server/pm/SaferIntentUtils.java b/services/core/java/com/android/server/pm/SaferIntentUtils.java
index 8175321..9a7ba0f 100644
--- a/services/core/java/com/android/server/pm/SaferIntentUtils.java
+++ b/services/core/java/com/android/server/pm/SaferIntentUtils.java
@@ -104,6 +104,7 @@
     @Disabled
     private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188;
 
+    @Nullable
     private static ParsedMainComponent infoToComponent(
             ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver) {
         if (info instanceof ActivityInfo) {
@@ -186,7 +187,7 @@
         }
 
         boolean isChangeEnabled(long changeId) {
-            return platformCompat == null || platformCompat.isChangeEnabledByUidInternal(
+            return platformCompat == null || platformCompat.isChangeEnabledByUidInternalNoLogging(
                     changeId, callingUid);
         }
 
@@ -233,7 +234,8 @@
                 }
                 final ParsedMainComponent comp = infoToComponent(
                         resolveInfo.getComponentInfo(), resolver, args.isReceiver);
-                if (!comp.getIntents().isEmpty() && args.intent.getAction() == null) {
+                if (comp != null && !comp.getIntents().isEmpty()
+                        && args.intent.getAction() == null) {
                     match = false;
                 }
             } else if (c instanceof IntentFilter) {
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java
index ad146af..fb54c5d 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java
@@ -20,6 +20,8 @@
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
 
+import com.android.server.power.optimization.Flags;
+
 public class BatteryStatsDumpHelperImpl implements BatteryStats.BatteryStatsDumpHelper {
     private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;
 
@@ -33,6 +35,9 @@
                 .setMaxStatsAgeMs(0);
         if (detailed) {
             builder.includePowerModels().includeProcessStateData().includeVirtualUids();
+            if (Flags.batteryUsageStatsByPowerAndScreenState()) {
+                builder.includePowerStateData().includeScreenStateData();
+            }
         }
         return mBatteryUsageStatsProvider.getBatteryUsageStats((BatteryStatsImpl) batteryStats,
                 builder.build());
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 1b7bf89..4052a64 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -16356,6 +16356,7 @@
         mBluetoothPowerStatsCollector.collectAndDump(pw);
         mCameraPowerStatsCollector.collectAndDump(pw);
         mGnssPowerStatsCollector.collectAndDump(pw);
+        mCustomEnergyConsumerPowerStatsCollector.collectAndDump(pw);
     }
 
     private final Runnable mWriteAsyncRunnable = () -> {
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 8127b82..ac68966 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -201,7 +201,8 @@
 
             batteryUsageStatsBuilder = new BatteryUsageStats.Builder(
                     stats.getCustomEnergyConsumerNames(), includePowerModels,
-                    includeProcessStateData, minConsumedPowerThreshold);
+                    includeProcessStateData, query.isScreenStateDataNeeded(),
+                    query.isPowerStateDataNeeded(), minConsumedPowerThreshold);
 
             // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration
             // of batteryUsageStats sessions to wall-clock adjustments
@@ -348,6 +349,7 @@
         final String[] customEnergyConsumerNames = stats.getCustomEnergyConsumerNames();
         final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
                 customEnergyConsumerNames, includePowerModels, includeProcessStateData,
+                query.isScreenStateDataNeeded(), query.isPowerStateDataNeeded(),
                 minConsumedPowerThreshold);
         if (mPowerStatsStore == null) {
             Log.e(TAG, "PowerStatsStore is unavailable");
@@ -408,7 +410,6 @@
                             + " does not include process state data");
                     continue;
                 }
-
                 builder.add(snapshot);
             }
         }
diff --git a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
index 1191901..0273ba6 100644
--- a/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/CustomEnergyConsumerPowerStatsCollector.java
@@ -19,6 +19,7 @@
 import android.hardware.power.stats.EnergyConsumerType;
 import android.os.BatteryConsumer;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -65,4 +66,12 @@
         }
         return success;
     }
+
+    @Override
+    public void collectAndDump(PrintWriter pw) {
+        ensureInitialized();
+        for (int i = 0; i < mCollectors.size(); i++) {
+            mCollectors.get(i).collectAndDump(pw);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
index cace941..ce11fa0 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
@@ -176,9 +176,12 @@
 
                 for (EnergyConsumerAttribution attribution : perUid) {
                     int uid = mUidResolver.mapUid(attribution.uid);
-                    long lastEnergy = mLastConsumerEnergyPerUid.get(uid);
-                    long deltaEnergy = attribution.energyUWs - lastEnergy;
+                    long lastEnergy = mLastConsumerEnergyPerUid.get(uid, ENERGY_UNSPECIFIED);
                     mLastConsumerEnergyPerUid.put(uid, attribution.energyUWs);
+                    if (lastEnergy == ENERGY_UNSPECIFIED) {
+                        continue;
+                    }
+                    long deltaEnergy = attribution.energyUWs - lastEnergy;
                     if (deltaEnergy <= 0) {
                         continue;
                     }
@@ -189,7 +192,8 @@
                     }
 
                     mLayout.setUidConsumedEnergy(uidStats, 0,
-                            mLayout.getUidConsumedEnergy(uidStats, 0) + deltaEnergy);
+                            mLayout.getUidConsumedEnergy(uidStats, 0)
+                                    + uJtoUc(deltaEnergy, averageVoltage));
                 }
             }
         }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
index d9f6c1f..f5b0005 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
@@ -196,12 +196,11 @@
         }
 
         IndentingPrintWriter out = new IndentingPrintWriter(pw);
-        out.print(getClass().getSimpleName());
         if (!isEnabled()) {
+            out.print(getClass().getSimpleName());
             out.println(": disabled");
             return;
         }
-        out.println();
 
         ArrayList<PowerStats> collected = new ArrayList<>();
         Consumer<PowerStats> consumer = collected::add;
@@ -215,11 +214,9 @@
             removeConsumer(consumer);
         }
 
-        out.increaseIndent();
         for (PowerStats stats : collected) {
             stats.dump(out);
         }
-        out.decreaseIndent();
     }
 
     private void awaitCompletion() {
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
index 549a97e..0f13492 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java
@@ -129,17 +129,55 @@
         if (descriptor == null) {
             return;
         }
+        boolean isCustomComponent =
+                descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
 
         PowerStatsLayout layout = new PowerStatsLayout();
         layout.fromExtras(descriptor.extras);
 
         long[] deviceStats = new long[descriptor.statsArrayLength];
+        for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) {
+            if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) {
+                if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                    continue;
+                }
+            } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                continue;
+            }
+
+            for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) {
+                if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) {
+                    if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                        continue;
+                    }
+                } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) {
+                    continue;
+                }
+
+                populateAggregatedBatteryConsumer(batteryUsageStatsBuilder, powerComponentStats,
+                        layout, deviceStats, screenState, powerState);
+            }
+        }
+        if (layout.isUidPowerAttributionSupported()) {
+            populateBatteryConsumers(batteryUsageStatsBuilder,
+                    powerComponentStats, layout);
+        }
+    }
+
+    private static void populateAggregatedBatteryConsumer(
+            BatteryUsageStats.Builder batteryUsageStatsBuilder,
+            PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout,
+            long[] deviceStats, @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        int powerComponentId = powerComponentStats.powerComponentId;
+        boolean isCustomComponent =
+                powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+
         double[] totalPower = new double[1];
         MultiStateStats.States.forEachTrackedStateCombination(
                 powerComponentStats.getConfig().getDeviceStateConfig(),
                 states -> {
-                    if (states[AggregatedPowerStatsConfig.STATE_POWER]
-                            != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) {
+                    if (!areMatchingStates(states, screenState, powerState)) {
                         return;
                     }
 
@@ -153,24 +191,23 @@
         AggregateBatteryConsumer.Builder deviceScope =
                 batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder(
                         BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
-        if (descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
-            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(
-                    descriptor.powerComponentId)) {
-                deviceScope.addConsumedPowerForCustomComponent(descriptor.powerComponentId,
-                        totalPower[0]);
+        if (isCustomComponent) {
+            if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) {
+                deviceScope.addConsumedPowerForCustomComponent(powerComponentId, totalPower[0]);
             }
         } else {
-            deviceScope.addConsumedPower(descriptor.powerComponentId,
-                    totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED);
-        }
-
-        if (layout.isUidPowerAttributionSupported()) {
-            populateUidBatteryConsumers(batteryUsageStatsBuilder,
-                    powerComponentStats, layout);
+            BatteryConsumer.Key key = deviceScope.getKey(powerComponentId,
+                    BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
+            if (key != null) {
+                deviceScope.addConsumedPower(key, totalPower[0],
+                        BatteryConsumer.POWER_MODEL_UNDEFINED);
+            }
+            deviceScope.addConsumedPower(powerComponentId, totalPower[0],
+                    BatteryConsumer.POWER_MODEL_UNDEFINED);
         }
     }
 
-    private static void populateUidBatteryConsumers(
+    private static void populateBatteryConsumers(
             BatteryUsageStats.Builder batteryUsageStatsBuilder,
             PowerComponentAggregatedPowerStats powerComponentStats,
             PowerStatsLayout layout) {
@@ -185,11 +222,44 @@
                 .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked()
                 && powerComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
 
+        ArrayList<Integer> uids = new ArrayList<>();
+        powerComponentStats.collectUids(uids);
+        for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) {
+            if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) {
+                if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                    continue;
+                }
+            } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) {
+                continue;
+            }
+
+            for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) {
+                if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
+                    if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) {
+                        continue;
+                    }
+                } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) {
+                    continue;
+                }
+
+                populateUidBatteryConsumers(batteryUsageStatsBuilder, powerComponentStats, layout,
+                        uids, powerComponent, uidStats, breakDownByProcState, screenState,
+                        powerState);
+            }
+        }
+    }
+
+    private static void populateUidBatteryConsumers(
+            BatteryUsageStats.Builder batteryUsageStatsBuilder,
+            PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout,
+            List<Integer> uids, AggregatedPowerStatsConfig.PowerComponent powerComponent,
+            long[] uidStats, boolean breakDownByProcState,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        int powerComponentId = powerComponentStats.powerComponentId;
         double[] powerByProcState =
                 new double[breakDownByProcState ? BatteryConsumer.PROCESS_STATE_COUNT : 1];
         double powerAllApps = 0;
-        ArrayList<Integer> uids = new ArrayList<>();
-        powerComponentStats.collectUids(uids);
         for (int uid : uids) {
             UidBatteryConsumer.Builder builder =
                     batteryUsageStatsBuilder.getOrCreateUidBatteryConsumerBuilder(uid);
@@ -199,8 +269,7 @@
             MultiStateStats.States.forEachTrackedStateCombination(
                     powerComponent.getUidStateConfig(),
                     states -> {
-                        if (states[AggregatedPowerStatsConfig.STATE_POWER]
-                                != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) {
+                        if (!areMatchingStates(states, screenState, powerState)) {
                             return;
                         }
 
@@ -224,8 +293,17 @@
                 powerAllProcStates += power;
                 if (breakDownByProcState
                         && procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
-                    builder.addConsumedPower(builder.getKey(powerComponentId, procState), power,
-                            BatteryConsumer.POWER_MODEL_UNDEFINED);
+                    if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) {
+                        builder.addConsumedPower(
+                                builder.getKey(powerComponentId, procState, screenState,
+                                        powerState),
+                                power, BatteryConsumer.POWER_MODEL_UNDEFINED);
+                    } else {
+                        builder.addConsumedPower(
+                                builder.getKey(powerComponentId, procState, screenState,
+                                        BatteryConsumer.POWER_STATE_UNSPECIFIED),
+                                power, BatteryConsumer.POWER_MODEL_UNDEFINED);
+                    }
                 }
             }
             if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
@@ -243,8 +321,49 @@
         if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) {
             allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps);
         } else {
+            BatteryConsumer.Key key = allAppsScope.getKey(powerComponentId,
+                    BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState);
+            if (key != null) {
+                allAppsScope.addConsumedPower(key, powerAllApps,
+                        BatteryConsumer.POWER_MODEL_UNDEFINED);
+            }
             allAppsScope.addConsumedPower(powerComponentId, powerAllApps,
                     BatteryConsumer.POWER_MODEL_UNDEFINED);
         }
     }
+
+    private static boolean areMatchingStates(int[] states,
+            @BatteryConsumer.ScreenState int screenState,
+            @BatteryConsumer.PowerState int powerState) {
+        switch (screenState) {
+            case BatteryConsumer.SCREEN_STATE_ON:
+                if (states[AggregatedPowerStatsConfig.STATE_SCREEN]
+                        != AggregatedPowerStatsConfig.SCREEN_STATE_ON) {
+                    return false;
+                }
+                break;
+            case BatteryConsumer.SCREEN_STATE_OTHER:
+                if (states[AggregatedPowerStatsConfig.STATE_SCREEN]
+                        != AggregatedPowerStatsConfig.SCREEN_STATE_OTHER) {
+                    return false;
+                }
+                break;
+        }
+
+        switch (powerState) {
+            case BatteryConsumer.POWER_STATE_BATTERY:
+                if (states[AggregatedPowerStatsConfig.STATE_POWER]
+                        != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) {
+                    return false;
+                }
+                break;
+            case BatteryConsumer.POWER_STATE_OTHER:
+                if (states[AggregatedPowerStatsConfig.STATE_POWER]
+                        != AggregatedPowerStatsConfig.POWER_STATE_OTHER) {
+                    return false;
+                }
+                break;
+        }
+        return true;
+    }
 }
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index d34498a..cc0a283 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -54,3 +54,17 @@
     description: "Adds battery_usage_stats_slice atom"
     bug: "324602949"
 }
+
+flag {
+    name: "battery_usage_stats_by_power_and_screen_state"
+    namespace: "backstage_power"
+    description: "Batterystats dumpsys is enhanced by including power break-down by power s"
+    bug: "352835319"
+}
+
+flag {
+    name: "disable_composite_battery_usage_stats_atoms"
+    namespace: "backstage_power"
+    description: "Disable deprecated BatteryUsageStatsAtom pulled atom"
+    bug: "324602949"
+}
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 6537228..5fab13b 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -67,6 +67,7 @@
         CANCELLED_BY_SCREEN_OFF(VibrationProto.CANCELLED_BY_SCREEN_OFF),
         CANCELLED_BY_SETTINGS_UPDATE(VibrationProto.CANCELLED_BY_SETTINGS_UPDATE),
         CANCELLED_BY_USER(VibrationProto.CANCELLED_BY_USER),
+        CANCELLED_BY_FOREGROUND_USER(VibrationProto.CANCELLED_BY_FOREGROUND_USER),
         CANCELLED_BY_UNKNOWN_REASON(VibrationProto.CANCELLED_BY_UNKNOWN_REASON),
         CANCELLED_SUPERSEDED(VibrationProto.CANCELLED_SUPERSEDED),
         CANCELLED_BY_APP_OPS(VibrationProto.CANCELLED_BY_APP_OPS),
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 4437a2d..bff175f 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.vibrator;
 
+import static android.os.VibrationAttributes.USAGE_CLASS_ALARM;
 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
 
@@ -73,6 +74,7 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.SystemService;
+import com.android.server.pm.BackgroundUserSoundNotifier;
 
 import libcore.util.NativeAllocationRegistry;
 
@@ -173,7 +175,8 @@
     @GuardedBy("mLock")
     @Nullable private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider;
 
-    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+    @VisibleForTesting
+    BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
@@ -190,6 +193,19 @@
                                 /* immediate= */ false);
                     }
                 }
+            } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()
+                    && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) {
+                synchronized (mLock) {
+                    if (shouldCancelOnFgUserRequest(mNextVibration)) {
+                        clearNextVibrationLocked(new Vibration.EndInfo(
+                                Vibration.Status.CANCELLED_BY_FOREGROUND_USER));
+                    }
+                    if (shouldCancelOnFgUserRequest(mCurrentVibration)) {
+                        mCurrentVibration.notifyCancelled(new Vibration.EndInfo(
+                                        Vibration.Status.CANCELLED_BY_FOREGROUND_USER),
+                                /* immediate= */ false);
+                    }
+                }
             }
         }
     };
@@ -299,6 +315,9 @@
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_SCREEN_OFF);
+        if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers()) {
+            filter.addAction(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND);
+        }
         context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
 
         injector.addService(EXTERNAL_VIBRATOR_SERVICE, new ExternalVibratorService());
@@ -1423,6 +1442,14 @@
     }
 
     @GuardedBy("mLock")
+    private boolean shouldCancelOnFgUserRequest(@Nullable VibrationStepConductor conductor) {
+        if (conductor == null) {
+            return false;
+        }
+        return conductor.getVibration().callerInfo.attrs.getUsageClass() == USAGE_CLASS_ALARM;
+    }
+
+    @GuardedBy("mLock")
     private void onAllVibratorsLocked(Consumer<VibratorController> consumer) {
         for (int i = 0; i < mVibrators.size(); i++) {
             consumer.accept(mVibrators.valueAt(i));
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index a821f545..c4d601d 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -67,19 +67,13 @@
     private static final String TAG_SIGNATURE = "signature";
     private static final String TAG_FALLBACK = "isFallback";
     private static final String PIN_GROUP = "webview";
+
+    private final Context mContext;
     private final WebViewProviderInfo[] mWebViewProviderPackages;
 
-    // Initialization-on-demand holder idiom for getting the WebView provider packages once and
-    // for all in a thread-safe manner.
-    private static class LazyHolder {
-        private static final SystemImpl INSTANCE = new SystemImpl();
-    }
+    SystemImpl(Context context) {
+        mContext = context;
 
-    public static SystemImpl getInstance() {
-        return LazyHolder.INSTANCE;
-    }
-
-    private SystemImpl() {
         int numFallbackPackages = 0;
         int numAvailableByDefaultPackages = 0;
         XmlResourceParser parser = null;
@@ -184,14 +178,14 @@
     }
 
     @Override
-    public String getUserChosenWebViewProvider(Context context) {
-        return Settings.Global.getString(context.getContentResolver(),
+    public String getUserChosenWebViewProvider() {
+        return Settings.Global.getString(mContext.getContentResolver(),
                 Settings.Global.WEBVIEW_PROVIDER);
     }
 
     @Override
-    public void updateUserSetting(Context context, String newProviderName) {
-        Settings.Global.putString(context.getContentResolver(),
+    public void updateUserSetting(String newProviderName) {
+        Settings.Global.putString(mContext.getContentResolver(),
                 Settings.Global.WEBVIEW_PROVIDER,
                 newProviderName == null ? "" : newProviderName);
     }
@@ -207,8 +201,8 @@
     }
 
     @Override
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable) {
-        UserManager userManager = (UserManager)context.getSystemService(Context.USER_SERVICE);
+    public void enablePackageForAllUsers(String packageName, boolean enable) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         for(UserInfo userInfo : userManager.getUsers()) {
             enablePackageForUser(packageName, enable, userInfo.id);
         }
@@ -228,16 +222,15 @@
     }
 
     @Override
-    public void installExistingPackageForAllUsers(Context context, String packageName) {
-        UserManager userManager = context.getSystemService(UserManager.class);
+    public void installExistingPackageForAllUsers(String packageName) {
+        UserManager userManager = mContext.getSystemService(UserManager.class);
         for (UserInfo userInfo : userManager.getUsers()) {
             installPackageForUser(packageName, userInfo.id);
         }
     }
 
     private void installPackageForUser(String packageName, int userId) {
-        final Context context = AppGlobals.getInitialApplication();
-        final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0);
+        final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0);
         final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller();
         installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null);
     }
@@ -255,29 +248,28 @@
     }
 
     @Override
-    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
-            WebViewProviderInfo configInfo) {
-        return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS);
+    public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo) {
+        return UserPackage.getPackageInfosAllUsers(mContext, configInfo.packageName, PACKAGE_FLAGS);
     }
 
     @Override
-    public int getMultiProcessSetting(Context context) {
+    public int getMultiProcessSetting() {
         if (updateServiceV2()) {
             throw new IllegalStateException(
                     "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
         }
         return Settings.Global.getInt(
-                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
+                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
     }
 
     @Override
-    public void setMultiProcessSetting(Context context, int value) {
+    public void setMultiProcessSetting(int value) {
         if (updateServiceV2()) {
             throw new IllegalStateException(
                     "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
         }
         Settings.Global.putInt(
-                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
+                mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java
index ad32f62..3b77d07 100644
--- a/services/core/java/com/android/server/webkit/SystemInterface.java
+++ b/services/core/java/com/android/server/webkit/SystemInterface.java
@@ -16,7 +16,6 @@
 
 package com.android.server.webkit;
 
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -34,19 +33,19 @@
  * @hide
  */
 public interface SystemInterface {
-    public WebViewProviderInfo[] getWebViewPackages();
-    public int onWebViewProviderChanged(PackageInfo packageInfo);
-    public long getFactoryPackageVersion(String packageName) throws NameNotFoundException;
+    WebViewProviderInfo[] getWebViewPackages();
+    int onWebViewProviderChanged(PackageInfo packageInfo);
+    long getFactoryPackageVersion(String packageName) throws NameNotFoundException;
 
-    public String getUserChosenWebViewProvider(Context context);
-    public void updateUserSetting(Context context, String newProviderName);
-    public void killPackageDependents(String packageName);
+    String getUserChosenWebViewProvider();
+    void updateUserSetting(String newProviderName);
+    void killPackageDependents(String packageName);
 
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable);
-    public void installExistingPackageForAllUsers(Context context, String packageName);
+    void enablePackageForAllUsers(String packageName, boolean enable);
+    void installExistingPackageForAllUsers(String packageName);
 
-    public boolean systemIsDebuggable();
-    public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
+    boolean systemIsDebuggable();
+    PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo)
             throws NameNotFoundException;
     /**
      * Get the PackageInfos of all users for the package represented by {@param configInfo}.
@@ -54,15 +53,14 @@
      *         certain user. The returned array can contain null PackageInfos if the given package
      *         is uninstalled for some user.
      */
-    public List<UserPackage> getPackageInfoForProviderAllUsers(Context context,
-            WebViewProviderInfo configInfo);
+    List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo);
 
-    public int getMultiProcessSetting(Context context);
-    public void setMultiProcessSetting(Context context, int value);
-    public void notifyZygote(boolean enableMultiProcess);
+    int getMultiProcessSetting();
+    void setMultiProcessSetting(int value);
+    void notifyZygote(boolean enableMultiProcess);
     /** Start the zygote if it's not already running. */
-    public void ensureZygoteStarted();
-    public boolean isMultiProcessDefaultEnabled();
+    void ensureZygoteStarted();
+    boolean isMultiProcessDefaultEnabled();
 
-    public void pinWebviewIfRequired(ApplicationInfo appInfo);
+    void pinWebviewIfRequired(ApplicationInfo appInfo);
 }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index 043470f..7acb864 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -73,9 +73,9 @@
     public WebViewUpdateService(Context context) {
         super(context);
         if (updateServiceV2()) {
-            mImpl = new WebViewUpdateServiceImpl2(context, SystemImpl.getInstance());
+            mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context));
         } else {
-            mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance());
+            mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context));
         }
     }
 
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
index dcf20f9..b9be4a2 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
@@ -16,7 +16,6 @@
 package com.android.server.webkit;
 
 import android.annotation.Nullable;
-import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.Signature;
@@ -92,7 +91,6 @@
     private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE;
 
     private final SystemInterface mSystemInterface;
-    private final Context mContext;
 
     private long mMinimumVersionCode = -1;
 
@@ -110,8 +108,7 @@
 
     private final Object mLock = new Object();
 
-    WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) {
-        mContext = context;
+    WebViewUpdateServiceImpl(SystemInterface systemInterface) {
         mSystemInterface = systemInterface;
     }
 
@@ -173,7 +170,7 @@
         try {
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
-                String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
+                String userSetting = mSystemInterface.getUserChosenWebViewProvider();
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
                     // Don't persist the user-chosen setting across boots if the package being
@@ -181,8 +178,7 @@
                     // be surprised by the device switching to using a certain webview package,
                     // that was uninstalled/disabled a long time ago, if it is installed/enabled
                     // again.
-                    mSystemInterface.updateUserSetting(mContext,
-                            mCurrentWebViewPackage.packageName);
+                    mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName);
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
@@ -203,8 +199,7 @@
             WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
             if (fallbackProvider != null) {
                 Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
-                mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName,
-                                                          true);
+                mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true);
             } else {
                 Slog.e(TAG, "No valid provider and no fallback available.");
             }
@@ -316,7 +311,7 @@
             oldPackage = mCurrentWebViewPackage;
 
             if (newProviderName != null) {
-                mSystemInterface.updateUserSetting(mContext, newProviderName);
+                mSystemInterface.updateUserSetting(newProviderName);
             }
 
             try {
@@ -447,7 +442,7 @@
     private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
         ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
 
-        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider();
 
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
@@ -455,7 +450,7 @@
             if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
                 // userPackages can contain null objects.
                 List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
+                        mSystemInterface.getPackageInfoForProviderAllUsers(
                                 providerAndPackage.provider);
                 if (isInstalledAndEnabledForAllUsers(userPackages)) {
                     return providerAndPackage.packageInfo;
@@ -470,7 +465,7 @@
             if (providerAndPackage.provider.availableByDefault) {
                 // userPackages can contain null objects.
                 List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
+                        mSystemInterface.getPackageInfoForProviderAllUsers(
                                 providerAndPackage.provider);
                 if (isInstalledAndEnabledForAllUsers(userPackages)) {
                     return providerAndPackage.packageInfo;
@@ -658,7 +653,7 @@
 
     @Override
     public boolean isMultiProcessEnabled() {
-        int settingValue = mSystemInterface.getMultiProcessSetting(mContext);
+        int settingValue = mSystemInterface.getMultiProcessSetting();
         if (mSystemInterface.isMultiProcessDefaultEnabled()) {
             // Multiprocess should be enabled unless the user has turned it off manually.
             return settingValue > MULTIPROCESS_SETTING_OFF_VALUE;
@@ -671,7 +666,7 @@
     @Override
     public void enableMultiProcess(boolean enable) {
         PackageInfo current = getCurrentWebViewPackage();
-        mSystemInterface.setMultiProcessSetting(mContext,
+        mSystemInterface.setMultiProcessSetting(
                 enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE);
         mSystemInterface.notifyZygote(enable);
         if (current != null) {
@@ -725,7 +720,7 @@
         pw.println("  WebView packages:");
         for (WebViewProviderInfo provider : allProviders) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(provider);
             PackageInfo systemUserPackageInfo =
                     userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
             if (systemUserPackageInfo == null) {
@@ -741,7 +736,7 @@
                     systemUserPackageInfo.applicationInfo.targetSdkVersion);
             if (validity == VALIDITY_OK) {
                 boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider));
+                        mSystemInterface.getPackageInfoForProviderAllUsers(provider));
                 pw.println(String.format(
                         "    Valid package %s (%s) is %s installed/enabled for all users",
                         systemUserPackageInfo.packageName,
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 993597e..307c15b 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -16,7 +16,6 @@
 package com.android.server.webkit;
 
 import android.annotation.Nullable;
-import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.Signature;
@@ -86,7 +85,6 @@
     private static final int VALIDITY_NO_LIBRARY_FLAG = 4;
 
     private final SystemInterface mSystemInterface;
-    private final Context mContext;
     private final WebViewProviderInfo mDefaultProvider;
 
     private long mMinimumVersionCode = -1;
@@ -108,8 +106,7 @@
 
     private final Object mLock = new Object();
 
-    WebViewUpdateServiceImpl2(Context context, SystemInterface systemInterface) {
-        mContext = context;
+    WebViewUpdateServiceImpl2(SystemInterface systemInterface) {
         mSystemInterface = systemInterface;
         WebViewProviderInfo[] webviewProviders = getWebViewPackages();
 
@@ -194,8 +191,7 @@
         }
         if (mCurrentWebViewPackage.packageName.equals(mDefaultProvider.packageName)) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(
-                            mContext, mDefaultProvider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(mDefaultProvider);
             return !isInstalledAndEnabledForAllUsers(userPackages);
         } else {
             return false;
@@ -216,10 +212,8 @@
                 TAG,
                 "No provider available for all users, trying to install and enable "
                         + mDefaultProvider.packageName);
-        mSystemInterface.installExistingPackageForAllUsers(
-                mContext, mDefaultProvider.packageName);
-        mSystemInterface.enablePackageForAllUsers(
-                mContext, mDefaultProvider.packageName, true);
+        mSystemInterface.installExistingPackageForAllUsers(mDefaultProvider.packageName);
+        mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true);
     }
 
     @Override
@@ -229,7 +223,7 @@
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
                 repairNeeded = shouldTriggerRepairLocked();
-                String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
+                String userSetting = mSystemInterface.getUserChosenWebViewProvider();
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
                     // Don't persist the user-chosen setting across boots if the package being
@@ -237,8 +231,7 @@
                     // be surprised by the device switching to using a certain webview package,
                     // that was uninstalled/disabled a long time ago, if it is installed/enabled
                     // again.
-                    mSystemInterface.updateUserSetting(mContext,
-                            mCurrentWebViewPackage.packageName);
+                    mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName);
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
@@ -362,7 +355,7 @@
             oldPackage = mCurrentWebViewPackage;
 
             if (newProviderName != null) {
-                mSystemInterface.updateUserSetting(mContext, newProviderName);
+                mSystemInterface.updateUserSetting(newProviderName);
             }
 
             try {
@@ -493,7 +486,7 @@
         Counter.logIncrement("webview.value_find_preferred_webview_package_counter");
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
-        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider();
         WebViewProviderInfo userChosenProvider =
                 getWebViewProviderForPackage(userChosenPackageName);
         if (userChosenProvider != null) {
@@ -502,8 +495,7 @@
                         mSystemInterface.getPackageInfoForProvider(userChosenProvider);
                 if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) {
                     List<UserPackage> userPackages =
-                            mSystemInterface.getPackageInfoForProviderAllUsers(
-                                    mContext, userChosenProvider);
+                            mSystemInterface.getPackageInfoForProviderAllUsers(userChosenProvider);
                     if (isInstalledAndEnabledForAllUsers(userPackages)) {
                         return packageInfo;
                     }
@@ -779,7 +771,7 @@
         pw.println("  WebView packages:");
         for (WebViewProviderInfo provider : allProviders) {
             List<UserPackage> userPackages =
-                    mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider);
+                    mSystemInterface.getPackageInfoForProviderAllUsers(provider);
             PackageInfo systemUserPackageInfo =
                     userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
             if (systemUserPackageInfo == null) {
@@ -798,8 +790,7 @@
             if (validity == VALIDITY_OK) {
                 boolean installedForAllUsers =
                         isInstalledAndEnabledForAllUsers(
-                                mSystemInterface.getPackageInfoForProviderAllUsers(
-                                        mContext, provider));
+                                mSystemInterface.getPackageInfoForProviderAllUsers(provider));
                 pw.println(
                         TextUtils.formatSimple(
                                 "    Valid package %s (%s) is %s installed/enabled for all users",
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index 2d8aa3f..09de01e 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -137,7 +137,7 @@
     }
 
     abstract ActivityRecord getTopActivity(TYPE source);
-    abstract ActivityRecord getTopFullscreenActivity(TYPE source);
+    abstract WindowState getTopFullscreenWindow(TYPE source);
     abstract ActivityManager.TaskDescription getTaskDescription(TYPE source);
     /**
      * Find the window for a given task to take a snapshot. Top child of the task is usually the one
@@ -465,10 +465,7 @@
      */
     @WindowInsetsController.Appearance
     private int getAppearance(TYPE source) {
-        final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source);
-        final WindowState topFullscreenWindow = topFullscreenActivity != null
-                ? topFullscreenActivity.findMainWindow()
-                : null;
+        final WindowState topFullscreenWindow = getTopFullscreenWindow(source);
         if (topFullscreenWindow != null) {
             return topFullscreenWindow.mAttrs.insetsFlags.appearance;
         }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 03a1377..400919a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2867,7 +2867,10 @@
         if (mStartingData != null) {
             if (mStartingData.mAssociatedTask != null) {
                 // The snapshot type may have called associateStartingDataWithTask().
-                attachStartingSurfaceToAssociatedTask();
+                // If this activity is rotated, don't attach to task to preserve the transform.
+                if (!hasFixedRotationTransform()) {
+                    attachStartingSurfaceToAssociatedTask();
+                }
             } else if (isEmbedded()) {
                 associateStartingWindowWithTaskIfNeeded();
             }
@@ -2898,6 +2901,12 @@
                 || mStartingData.mAssociatedTask != null) {
             return;
         }
+        if (task.isVisible() && !task.inTransition()) {
+            // Don't associated with task if the task is visible especially when the activity is
+            // embedded. We just need to show splash screen on the activity in case the first frame
+            // is not ready.
+            return;
+        }
         associateStartingDataWithTask();
         attachStartingSurfaceToAssociatedTask();
     }
@@ -8964,7 +8973,7 @@
      * <p>Conditions that need to be met:
      *
      * <ul>
-     *     <li>{@link LetterboxConfiguration#getIsEducationEnabled} is true.
+     *     <li>{@link AppCompatConfiguration#getIsEducationEnabled} is true.
      *     <li>The activity is eligible for fixed orientation letterbox.
      *     <li>The activity is in fullscreen.
      *     <li>The activity is portrait-only.
@@ -8973,7 +8982,7 @@
      * </ul>
      */
     boolean isEligibleForLetterboxEducation() {
-        return mWmService.mLetterboxConfiguration.getIsEducationEnabled()
+        return mWmService.mAppCompatConfiguration.getIsEducationEnabled()
                 && mIsEligibleForFixedOrientationLetterbox
                 && getWindowingMode() == WINDOWING_MODE_FULLSCREEN
                 && getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT
diff --git a/services/core/java/com/android/server/wm/ActivityRefresher.java b/services/core/java/com/android/server/wm/ActivityRefresher.java
index 0c32dfc..bc82271 100644
--- a/services/core/java/com/android/server/wm/ActivityRefresher.java
+++ b/services/core/java/com/android/server/wm/ActivityRefresher.java
@@ -75,7 +75,7 @@
         }
 
         final boolean cycleThroughStop =
-                mWmService.mLetterboxConfiguration
+                mWmService.mAppCompatConfiguration
                         .isCameraCompatRefreshCycleThroughStopEnabled()
                         && !activity.mAppCompatController.getAppCompatCameraOverrides()
                             .shouldRefreshActivityViaPauseForCameraCompat();
@@ -114,7 +114,7 @@
 
     private boolean shouldRefreshActivity(@NonNull ActivityRecord activity,
             @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) {
-        return mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
+        return mWmService.mAppCompatConfiguration.isCameraCompatRefreshEnabled()
                 && activity.mAppCompatController.getAppCompatOverrides()
                     .getAppCompatCameraOverrides().shouldRefreshActivityForCameraCompat()
                 && ArrayUtils.find(mEvaluators.toArray(), evaluator ->
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 62fb4bf..48bc813 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -590,10 +590,8 @@
         return activity;
     }
 
-    @Override
-    ActivityRecord getTopFullscreenActivity(ActivityRecord activity) {
-        final WindowState win = activity.findMainWindow();
-        return (win != null && win.mAttrs.isFullscreen()) ? activity : null;
+    WindowState getTopFullscreenWindow(ActivityRecord activity) {
+        return activity.findMainWindow();
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 3b0b727..26a6b00 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -584,7 +584,7 @@
 
     public abstract void clearLockedTasks(String reason);
     public abstract void updateUserConfiguration();
-    public abstract boolean canShowErrorDialogs();
+    public abstract boolean canShowErrorDialogs(int userId);
 
     public abstract void setProfileApp(String profileApp);
     public abstract void setProfileProc(WindowProcessController wpc);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index ff46b33..a84598d 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -190,6 +190,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -4899,14 +4900,21 @@
      * dialog / global actions also might want different behaviors.
      */
     private void updateShouldShowDialogsLocked(Configuration config) {
+        mShowDialogs = shouldShowDialogs(config, /* checkUiMode= */ true);
+    }
+
+    private boolean shouldShowDialogs(Configuration config, boolean checkUiMode) {
         final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS
                 && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
                 && config.navigation == Configuration.NAVIGATION_NONAV);
         final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(),
                 HIDE_ERROR_DIALOGS, 0) != 0;
-        mShowDialogs = inputMethodExists
-                && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config)
-                && !hideDialogsSet;
+        boolean showDialogs = inputMethodExists && !hideDialogsSet;
+        if (checkUiMode) {
+            showDialogs = showDialogs
+                    && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config);
+        }
+        return showDialogs;
     }
 
     private void updateFontScaleIfNeeded(@UserIdInt int userId) {
@@ -7148,17 +7156,69 @@
         }
 
         @Override
-        public boolean canShowErrorDialogs() {
+        public boolean canShowErrorDialogs(int userId) {
             synchronized (mGlobalLock) {
-                return mShowDialogs && !mSleeping && !mShuttingDown
+                final boolean showDialogs = mShowDialogs
+                        || shouldShowDialogsForVisibleBackgroundUserLocked(userId);
+                final UserInfo userInfo = getUserManager().getUserInfo(userId);
+                if (userInfo == null) {
+                    // Unable to retrieve user information. Returning false, assuming there is
+                    // no valid user with the given id.
+                    return false;
+                }
+                return showDialogs && !mSleeping && !mShuttingDown
                         && !mKeyguardController.isKeyguardOrAodShowing(DEFAULT_DISPLAY)
-                        && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS,
-                        mAmInternal.getCurrentUserId())
+                        && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, userId)
                         && !(UserManager.isDeviceInDemoMode(mContext)
-                        && mAmInternal.getCurrentUser().isDemo());
+                        && userInfo.isDemo());
             }
         }
 
+        /**
+         * Checks if the given user is a visible background user, which is a full, background user
+         * assigned to secondary displays on the devices that have
+         * {@link UserManager#isVisibleBackgroundUsersEnabled()
+         * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on
+         * automotive builds, using the display associated with their seats).
+         *
+         * @see UserManager#isUserVisible()
+         */
+        private boolean isVisibleBackgroundUser(int userId) {
+            if (!UserManager.isVisibleBackgroundUsersEnabled()) {
+                return false;
+            }
+            boolean isForeground = getCurrentUserId() == userId;
+            boolean isProfile = getUserManager().isProfile(userId);
+            boolean isVisible = mWindowManager.mUmInternal.isUserVisible(userId);
+            return isVisible && !isForeground && !isProfile;
+        }
+
+        /**
+         * In a car environment, {@link ActivityTaskManagerService#mShowDialogs} is always set to
+         * {@code false} from {@link ActivityTaskManagerService#updateShouldShowDialogsLocked}
+         * because its UI mode is {@link Configuration#UI_MODE_TYPE_CAR}. Thus, error dialogs are
+         * not displayed when an ANR or a crash occurs. However, in the automotive multi-user
+         * multi-display environment, this can confuse the passenger users and leave them
+         * uninformed when an app is terminated by the ANR or crash without any notification.
+         * To address this, error dialogs are allowed for the passenger users who have UI access
+         * on assigned displays (a.k.a. visible background users) on devices that have
+         * config_multiuserVisibleBackgroundUsers enabled even though the UI mode is
+         * {@link Configuration#UI_MODE_TYPE_CAR}.
+         *
+         * @see ActivityTaskManagerService#updateShouldShowDialogsLocked
+         */
+        private boolean shouldShowDialogsForVisibleBackgroundUserLocked(int userId) {
+            if (!isVisibleBackgroundUser(userId)) {
+                return false;
+            }
+            final int displayId = mWindowManager.mUmInternal.getMainDisplayAssignedToUser(userId);
+            final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId);
+            if (dc == null) {
+                return false;
+            }
+            return shouldShowDialogs(dc.getConfiguration(), /* checkUiMode= */ false);
+        }
+
         @Override
         public void setProfileApp(String profileApp) {
             synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index cf008e7..05d4c82 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -34,8 +34,8 @@
 
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 
 import android.annotation.NonNull;
 import android.content.pm.PackageManager;
@@ -62,7 +62,7 @@
     @NonNull
     private final ActivityRecord mActivityRecord;
     @NonNull
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
     @NonNull
     private final UserAspectRatioState mUserAspectRatioState;
 
@@ -80,12 +80,12 @@
     private final Function<Configuration, Float> mGetHorizontalPositionMultiplierProvider;
 
     AppCompatAspectRatioOverrides(@NonNull ActivityRecord activityRecord,
-            @NonNull LetterboxConfiguration letterboxConfiguration,
+            @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder,
             @NonNull Function<Boolean, Boolean> isDisplayFullScreenAndInPostureProvider,
             @NonNull Function<Configuration, Float> getHorizontalPositionMultiplierProvider) {
         mActivityRecord = activityRecord;
-        mLetterboxConfiguration = letterboxConfiguration;
+        mAppCompatConfiguration = appCompatConfiguration;
         mUserAspectRatioState = new UserAspectRatioState();
         mIsDisplayFullScreenAndInPostureProvider = isDisplayFullScreenAndInPostureProvider;
         mGetHorizontalPositionMultiplierProvider = getHorizontalPositionMultiplierProvider;
@@ -93,10 +93,10 @@
                 PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
         mAllowUserAspectRatioOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE,
-                mLetterboxConfiguration::isUserAppAspectRatioSettingsEnabled);
+                mAppCompatConfiguration::isUserAppAspectRatioSettingsEnabled);
         mAllowUserAspectRatioFullscreenOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
-                mLetterboxConfiguration::isUserAppAspectRatioFullscreenEnabled);
+                mAppCompatConfiguration::isUserAppAspectRatioFullscreenEnabled);
         mAllowOrientationOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
     }
@@ -147,7 +147,7 @@
     boolean isUserFullscreenOverrideEnabled() {
         if (mAllowUserAspectRatioOverrideOptProp.isFalse()
                 || mAllowUserAspectRatioFullscreenOverrideOptProp.isFalse()
-                || !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
+                || !mAppCompatConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
             return false;
         }
         return true;
@@ -168,7 +168,7 @@
         // effect only if explicitly false. If mBooleanPropertyAllowUserAspectRatioOverride is null,
         // the current app doesn't opt-out so the first part of the predicate is true.
         return !mAllowUserAspectRatioOverrideOptProp.isFalse()
-                && mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()
+                && mAppCompatConfiguration.isUserAppAspectRatioSettingsEnabled()
                 && mActivityRecord.mDisplayContent != null
                 && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest();
     }
@@ -226,6 +226,14 @@
                         : getDefaultMinAspectRatio();
     }
 
+    float getDefaultMinAspectRatioForUnresizableAppsFromConfig() {
+        return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps();
+    }
+
+    boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() {
+        return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+    }
+
     private float getDisplaySizeMinAspectRatio() {
         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
         if (displayArea == null) {
@@ -267,22 +275,22 @@
     }
 
     private float getDefaultMinAspectRatioForUnresizableApps() {
-        if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
+        if (!mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
                 || mActivityRecord.getDisplayArea() == null) {
-            return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
+            return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps()
                     > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
-                    ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
+                    ? mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps()
                     : getDefaultMinAspectRatio();
         }
 
         return getSplitScreenAspectRatio();
     }
 
-    private float getDefaultMinAspectRatio() {
+    float getDefaultMinAspectRatio() {
         if (mActivityRecord.getDisplayArea() == null
-                || !mLetterboxConfiguration
+                || !mAppCompatConfiguration
                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
-            return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+            return mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio();
         }
         return getDisplaySizeMinAspectRatio();
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index a7d2ecc..b23e75a 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -26,8 +26,8 @@
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
-import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
-import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index 0d108e1..93a8663 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -52,7 +52,7 @@
     @NonNull
     private final AppCompatCameraOverridesState mAppCompatCameraOverridesState;
     @NonNull
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
     @NonNull
     private final OptPropFactory.OptProp mAllowMinAspectRatioOverrideOptProp;
     @NonNull
@@ -63,15 +63,15 @@
     private final OptPropFactory.OptProp mCameraCompatAllowForceRotationOptProp;
 
     AppCompatCameraOverrides(@NonNull ActivityRecord activityRecord,
-            @NonNull LetterboxConfiguration letterboxConfiguration,
+            @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder) {
         mActivityRecord = activityRecord;
-        mLetterboxConfiguration = letterboxConfiguration;
+        mAppCompatConfiguration = appCompatConfiguration;
         mAppCompatCameraOverridesState = new AppCompatCameraOverridesState();
         mAllowMinAspectRatioOverrideOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
         final BooleanSupplier isCameraCompatTreatmentEnabled = AppCompatUtils.asLazy(
-                mLetterboxConfiguration::isCameraCompatTreatmentEnabled);
+                mAppCompatConfiguration::isCameraCompatTreatmentEnabled);
         mCameraCompatAllowRefreshOptProp = optPropBuilder.create(
                 PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH,
                 isCameraCompatTreatmentEnabled);
@@ -214,7 +214,7 @@
      * is active because the corresponding config is enabled and activity supports resizing.
      */
     boolean isCameraCompatSplitScreenAspectRatioAllowed() {
-        return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
+        return mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled()
                 && !mActivityRecord.shouldCreateCompatDisplayInsets();
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index c8955fd..1562cf6 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -45,7 +45,7 @@
         // Not checking DeviceConfig value here to allow enabling via DeviceConfig
         // without the need to restart the device.
         final boolean needsDisplayRotationCompatPolicy =
-                wmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
+                wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
         final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
                 && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
         if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
similarity index 96%
rename from services/core/java/com/android/server/wm/LetterboxConfiguration.java
rename to services/core/java/com/android/server/wm/AppCompatConfiguration.java
index 0161ae5..ffa4251 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
@@ -34,10 +34,10 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.function.Function;
 
-/** Reads letterbox configs from resources and controls their overrides at runtime. */
-final class LetterboxConfiguration {
+/** Reads app compatibility configs from resources and controls their overrides at runtime. */
+final class AppCompatConfiguration {
 
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatConfiguration" : TAG_ATM;
 
     // Whether camera compatibility treatment is enabled.
     // See DisplayRotationCompatPolicy for context.
@@ -183,7 +183,7 @@
 
     // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
     @NonNull
-    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private final AppCompatConfigurationPersister mAppCompatConfigurationPersister;
 
     // Aspect ratio of letterbox for fixed orientation, values <=
     // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored.
@@ -307,8 +307,8 @@
     // Flags dynamically updated with {@link android.provider.DeviceConfig}.
     @NonNull private final SynchedDeviceConfig mDeviceConfig;
 
-    LetterboxConfiguration(@NonNull final Context systemUiContext) {
-        this(systemUiContext, new LetterboxConfigurationPersister(
+    AppCompatConfiguration(@NonNull final Context systemUiContext) {
+        this(systemUiContext, new AppCompatConfigurationPersister(
                 () -> readLetterboxHorizontalReachabilityPositionFromConfig(
                         systemUiContext, /* forBookMode */ false),
                 () -> readLetterboxVerticalReachabilityPositionFromConfig(
@@ -320,8 +320,8 @@
     }
 
     @VisibleForTesting
-    LetterboxConfiguration(@NonNull final Context systemUiContext,
-            @NonNull final LetterboxConfigurationPersister letterboxConfigurationPersister) {
+    AppCompatConfiguration(@NonNull final Context systemUiContext,
+            @NonNull final AppCompatConfigurationPersister appCompatConfigurationPersister) {
         mContext = systemUiContext;
 
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
@@ -370,8 +370,8 @@
         mThinLetterboxHeightPxSupplier = new DimenPxIntSupplier(mContext,
                 R.dimen.config_letterboxThinLetterboxHeightDp);
 
-        mLetterboxConfigurationPersister = letterboxConfigurationPersister;
-        mLetterboxConfigurationPersister.start();
+        mAppCompatConfigurationPersister = appCompatConfigurationPersister;
+        mAppCompatConfigurationPersister.start();
 
         mDeviceConfig = SynchedDeviceConfig.builder(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                         systemUiContext.getMainExecutor())
@@ -605,7 +605,7 @@
 
     /**
      * Overrides alpha of a black scrim shown over wallpaper for {@link
-     * #LETTERBOX_BACKGROUND_WALLPAPER} option returned from {@link getLetterboxBackgroundType()}.
+     * #LETTERBOX_BACKGROUND_WALLPAPER} option returned from {@link #getLetterboxBackgroundType()}.
      *
      * <p>If given value is < 0 or >= 1, both it and a value of {@link
      * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha} are ignored
@@ -632,8 +632,8 @@
     }
 
     /**
-     * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option from
-     * {@link getLetterboxBackgroundType()}.
+     * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option from {@link
+     * #getLetterboxBackgroundType()}.
      *
      * <p> If given value <= 0, both it and a value of {@link
      * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius} are ignored
@@ -645,7 +645,7 @@
 
     /**
      * Resets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link
-     * getLetterboxBackgroundType()} to {@link
+     * #getLetterboxBackgroundType()} to {@link
      * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius}.
      */
     void resetLetterboxBackgroundWallpaperBlurRadiusPx() {
@@ -654,14 +654,14 @@
     }
 
     /**
-     * Gets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link
-     * getLetterboxBackgroundType()}.
+     * Gets blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link
+     * #getLetterboxBackgroundType()}.
      */
     int getLetterboxBackgroundWallpaperBlurRadiusPx() {
         return mLetterboxBackgroundWallpaperBlurRadiusPx;
     }
 
-    /*
+    /**
      * Gets horizontal position of a center of the letterboxed app window specified
      * in {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}
      * or via an ADB command. 0 corresponds to the left side of the screen and 1 to the
@@ -672,7 +672,7 @@
                 : mLetterboxHorizontalPositionMultiplier;
     }
 
-    /*
+    /**
      * Gets vertical position of a center of the letterboxed app window specified
      * in {@link com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier}
      * or via an ADB command. 0 corresponds to the top side of the screen and 1 to the
@@ -743,7 +743,7 @@
                 "mLetterboxBookModePositionMultiplier");
     }
 
-    /*
+    /**
      * Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps in
      * landscape device orientation.
      */
@@ -751,7 +751,7 @@
         return mIsHorizontalReachabilityEnabled;
     }
 
-    /*
+    /**
      * Whether vertical reachability repositioning is allowed for letterboxed fullscreen apps in
      * portrait device orientation.
      */
@@ -759,7 +759,7 @@
         return mIsVerticalReachabilityEnabled;
     }
 
-    /*
+    /**
      * Whether automatic horizontal reachability repositioning in book mode is allowed for
      * letterboxed fullscreen apps in landscape device orientation.
      */
@@ -821,7 +821,7 @@
                 R.bool.config_letterboxIsAutomaticReachabilityInBookModeEnabled);
     }
 
-    /*
+    /**
      * Gets default horizontal position of the letterboxed app window when horizontal reachability
      * is enabled.
      *
@@ -833,7 +833,7 @@
         return mDefaultPositionForHorizontalReachability;
     }
 
-    /*
+    /**
      * Gets default vertical position of the letterboxed app window when vertical reachability is
      * enabled.
      *
@@ -889,7 +889,7 @@
      */
     void setPersistentLetterboxPositionForHorizontalReachability(boolean forBookMode,
             @LetterboxHorizontalReachabilityPosition int position) {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(
                 forBookMode, position);
     }
 
@@ -899,7 +899,7 @@
      */
     void setPersistentLetterboxPositionForVerticalReachability(boolean forTabletopMode,
             @LetterboxVerticalReachabilityPosition int position) {
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(
                 forTabletopMode, position);
     }
 
@@ -909,11 +909,11 @@
      * is enabled to default position.
      */
     void resetPersistentLetterboxPositionForHorizontalReachability() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(
                 false /* forBookMode */,
                 readLetterboxHorizontalReachabilityPositionFromConfig(mContext,
                         false /* forBookMode */));
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(
                 true /* forBookMode */,
                 readLetterboxHorizontalReachabilityPositionFromConfig(mContext,
                         true /* forBookMode */));
@@ -925,11 +925,11 @@
      * enabled to default position.
      */
     void resetPersistentLetterboxPositionForVerticalReachability() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(
                 false /* forTabletopMode */,
                 readLetterboxVerticalReachabilityPositionFromConfig(mContext,
                         false /* forTabletopMode */));
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(
                 true /* forTabletopMode */,
                 readLetterboxVerticalReachabilityPositionFromConfig(mContext,
                         true /* forTabletopMode */));
@@ -961,7 +961,7 @@
                     ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
     }
 
-    /*
+    /**
      * Gets horizontal position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the left side of the screen and 1 to the right side.
      *
@@ -969,7 +969,7 @@
      */
     float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) {
         final int letterboxPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
                         isDeviceInBookMode);
         switch (letterboxPositionForHorizontalReachability) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
@@ -985,7 +985,7 @@
         }
     }
 
-    /*
+    /**
      * Gets vertical position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side.
      *
@@ -993,7 +993,7 @@
      */
     float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) {
         final int letterboxPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(
                         isDeviceInTabletopMode);
         switch (letterboxPositionForVerticalReachability) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
@@ -1009,23 +1009,23 @@
         }
     }
 
-    /*
+    /**
      * Gets the horizontal position of the letterboxed app window when horizontal reachability is
      * enabled.
      */
     @LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability(boolean isInFullScreenBookMode) {
-        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+        return mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
                 isInFullScreenBookMode);
     }
 
-    /*
+    /**
      * Gets the vertical position of the letterboxed app window when vertical reachability is
      * enabled.
      */
     @LetterboxVerticalReachabilityPosition
     int getLetterboxPositionForVerticalReachability(boolean isInFullScreenTabletopMode) {
-        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+        return mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(
                 isInFullScreenTabletopMode);
     }
 
@@ -1197,6 +1197,10 @@
                 || mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY);
     }
 
+    /**
+     * Sets whether we use the constraints override strategy for letterboxing when dealing
+     * with translucent activities.
+     */
     void setTranslucentLetterboxingOverrideEnabled(
             boolean translucentLetterboxingOverrideEnabled) {
         mTranslucentLetterboxingOverrideEnabled = translucentLetterboxingOverrideEnabled;
@@ -1204,8 +1208,8 @@
 
     /**
      * Resets whether we use the constraints override strategy for letterboxing when dealing
-     * with translucent activities
-     * {@link mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY)}.
+     * with translucent activities, which means mDeviceConfig.getFlagValue(
+     * KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY) will be used.
      */
     void resetTranslucentLetterboxingEnabled() {
         setTranslucentLetterboxingOverrideEnabled(false);
@@ -1215,11 +1219,11 @@
     private void updatePositionForHorizontalReachability(boolean isDeviceInBookMode,
             Function<Integer, Integer> newHorizonalPositionFun) {
         final int letterboxPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
                         isDeviceInBookMode);
         final int nextHorizontalPosition = newHorizonalPositionFun.apply(
                 letterboxPositionForHorizontalReachability);
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(
                 isDeviceInBookMode, nextHorizontalPosition);
     }
 
@@ -1227,11 +1231,11 @@
     private void updatePositionForVerticalReachability(boolean isDeviceInTabletopMode,
             Function<Integer, Integer> newVerticalPositionFun) {
         final int letterboxPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(
                         isDeviceInTabletopMode);
         final int nextVerticalPosition = newVerticalPositionFun.apply(
                 letterboxPositionForVerticalReachability);
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+        mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(
                 isDeviceInTabletopMode, nextVerticalPosition);
     }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java
similarity index 96%
rename from services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
rename to services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java
index 38aa903..852ce04 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java
@@ -30,8 +30,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
-import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition;
+import com.android.server.wm.AppCompatConfiguration.LetterboxHorizontalReachabilityPosition;
+import com.android.server.wm.AppCompatConfiguration.LetterboxVerticalReachabilityPosition;
 import com.android.server.wm.nano.WindowManagerProtos;
 
 import java.io.ByteArrayOutputStream;
@@ -45,12 +45,12 @@
 
 /**
  * Persists the values of letterboxPositionForHorizontalReachability and
- * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}.
+ * letterboxPositionForVerticalReachability for {@link AppCompatConfiguration}.
  */
-class LetterboxConfigurationPersister {
+class AppCompatConfigurationPersister {
 
     private static final String TAG =
-            TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
+            TAG_WITH_CLASS_NAME ? "AppCompatConfigurationPersister" : TAG_WM;
 
     private static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
 
@@ -94,7 +94,7 @@
     @NonNull
     private final PersisterQueue mPersisterQueue;
 
-    LetterboxConfigurationPersister(
+    AppCompatConfigurationPersister(
             @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
             @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
             @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
@@ -106,7 +106,7 @@
     }
 
     @VisibleForTesting
-    LetterboxConfigurationPersister(
+    AppCompatConfigurationPersister(
             @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
             @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
             @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
@@ -233,7 +233,7 @@
                     letterboxData.letterboxPositionForTabletopModeReachability;
         } catch (IOException ioe) {
             Slog.e(TAG,
-                    "Error reading from LetterboxConfigurationPersister. "
+                    "Error reading from AppCompatConfigurationPersister. "
                             + "Using default values!", ioe);
             useDefaultValue();
         } finally {
@@ -242,7 +242,7 @@
                     fis.close();
                 } catch (IOException e) {
                     useDefaultValue();
-                    Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e);
+                    Slog.e(TAG, "Error reading from AppCompatConfigurationPersister ", e);
                 }
             }
         }
@@ -332,7 +332,7 @@
             } catch (IOException ioe) {
                 mFileToUpdate.failWrite(fos);
                 Slog.e(TAG,
-                        "Error writing to LetterboxConfigurationPersister. "
+                        "Error writing to AppCompatConfigurationPersister. "
                                 + "Using default values!", ioe);
             } finally {
                 if (mOnComplete != null) {
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 3eed96d..f9e2507 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -44,9 +44,9 @@
         final OptPropFactory optPropBuilder = new OptPropFactory(packageManager,
                 activityRecord.packageName);
         mTransparentPolicy = new TransparentPolicy(activityRecord,
-                wmService.mLetterboxConfiguration);
+                wmService.mAppCompatConfiguration);
         mAppCompatOverrides = new AppCompatOverrides(activityRecord,
-                wmService.mLetterboxConfiguration, optPropBuilder);
+                wmService.mAppCompatConfiguration, optPropBuilder);
         mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides);
         mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord,
                 mTransparentPolicy, mAppCompatOverrides);
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
index 9bf8011..0adf825 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java
@@ -59,7 +59,7 @@
     final OrientationOverridesState mOrientationOverridesState;
 
     AppCompatOrientationOverrides(@NonNull ActivityRecord activityRecord,
-            @NonNull LetterboxConfiguration letterboxConfiguration,
+            @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder,
             @NonNull AppCompatCameraOverrides appCompatCameraOverrides) {
         mActivityRecord = activityRecord;
@@ -67,7 +67,7 @@
         mOrientationOverridesState = new OrientationOverridesState(mActivityRecord,
                 System::currentTimeMillis);
         final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy(
-                letterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled);
+                appCompatConfiguration::isPolicyForIgnoringRequestedOrientationEnabled);
         mIgnoreRequestedOrientationOptProp = optPropBuilder.create(
                 PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION,
                 isPolicyForIgnoringRequestedOrientationEnabled);
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index f6f93f9..b611ba9 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -42,7 +42,7 @@
     private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatOverrides" : TAG_ATM;
 
     @NonNull
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
 
     @NonNull
     private final ActivityRecord mActivityRecord;
@@ -63,23 +63,23 @@
     private final AppCompatAspectRatioOverrides mAppCompatAspectRatioOverrides;
 
     AppCompatOverrides(@NonNull ActivityRecord activityRecord,
-            @NonNull LetterboxConfiguration letterboxConfiguration,
+            @NonNull AppCompatConfiguration appCompatConfiguration,
             @NonNull OptPropFactory optPropBuilder) {
-        mLetterboxConfiguration = letterboxConfiguration;
+        mAppCompatConfiguration = appCompatConfiguration;
         mActivityRecord = activityRecord;
 
         mAppCompatCameraOverrides = new AppCompatCameraOverrides(mActivityRecord,
-                mLetterboxConfiguration, optPropBuilder);
+                mAppCompatConfiguration, optPropBuilder);
         mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(mActivityRecord,
-                mLetterboxConfiguration, optPropBuilder, mAppCompatCameraOverrides);
+                mAppCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides);
         // TODO(b/341903757) Remove BooleanSuppliers after fixing dependency with reachability.
         mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord,
-                mLetterboxConfiguration, optPropBuilder,
+                mAppCompatConfiguration, optPropBuilder,
                 activityRecord.mLetterboxUiController::isDisplayFullScreenAndInPosture,
                 activityRecord.mLetterboxUiController::getHorizontalPositionMultiplier);
 
         mFakeFocusOptProp = optPropBuilder.create(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS,
-                mLetterboxConfiguration::isCompatFakeFocusEnabled);
+                mAppCompatConfiguration::isCompatFakeFocusEnabled);
 
 
         mAllowOrientationOverrideOptProp = optPropBuilder.create(
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index f9f5058..3ecdff6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -16,18 +16,33 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
+import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
 import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
 import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
+import android.app.AppCompatTaskInfo;
+import android.app.TaskInfo;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.SystemProperties;
 import android.util.Size;
 import android.view.Gravity;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
 import java.util.function.Consumer;
 
 /**
@@ -38,6 +53,8 @@
 
     public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
             .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
+    public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties
+            .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25);
 
     /**
      * Updates launch bounds for an activity with respect to its activity options, window layout,
@@ -48,12 +65,8 @@
             @NonNull Rect outBounds, @NonNull Consumer<String> logger) {
         // Use stable frame instead of raw frame to avoid launching freeform windows on top of
         // stable insets, which usually are system widgets such as sysbar & navbar.
-        final TaskDisplayArea displayArea = task.getDisplayArea();
-        final Rect screenBounds = displayArea.getBounds();
         final Rect stableBounds = new Rect();
-        displayArea.getStableRect(stableBounds);
-        final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        task.getDisplayArea().getStableRect(stableBounds);
 
         if (options != null && options.getLaunchBounds() != null) {
             outBounds.set(options.getLaunchBounds());
@@ -63,37 +76,282 @@
             final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
             if (layout.hasSpecifiedSize()) {
                 calculateLayoutBounds(stableBounds, layout, outBounds,
-                        new Size(desiredWidth, desiredHeight));
+                        calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies sizes, inheriting size and applying gravity");
             } else if (verticalGravity > 0 || horizontalGravity > 0) {
-                calculateAndCentreInitialBounds(outBounds, screenBounds);
+                outBounds.set(calculateInitialBounds(task, activity, stableBounds));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies gravity, applying desired bounds and gravity");
             }
         } else {
-            calculateAndCentreInitialBounds(outBounds, screenBounds);
+            outBounds.set(calculateInitialBounds(task, activity, stableBounds));
             logger.accept("layout not specified, applying desired bounds");
         }
     }
 
     /**
-     * Calculates the initial height and width of a task in desktop mode and centers it within the
-     * window bounds.
+     * Calculates the initial bounds required for an application to fill a scale of the display
+     * bounds without any letterboxing. This is done by taking into account the applications
+     * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is
+     * compatible with the applications previous configuration.
      */
-    private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds,
+    private static @NonNull Rect calculateInitialBounds(@NonNull Task task,
+            @NonNull ActivityRecord activity, @NonNull Rect stableBounds
+    ) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        // Display bounds not taking into account insets.
+        final TaskDisplayArea displayArea = task.getDisplayArea();
+        final Rect screenBounds = displayArea.getBounds();
+        final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        if (!Flags.enableWindowingDynamicInitialBounds()) {
+            return centerInScreen(idealSize, screenBounds);
+        }
+        // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete.
+        float appAspectRatio = calculateAspectRatio(task, activity);
+        final float tdaWidth = stableBounds.width();
+        final float tdaHeight = stableBounds.height();
+        final int activityOrientation = activity.getOverrideOrientation();
+        final Size initialSize = switch (taskInfo.configuration.orientation) {
+            case ORIENTATION_LANDSCAPE -> {
+                // Device in landscape orientation.
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationPortrait(activityOrientation)) {
+                        // For portrait resizeable activities, respect apps fullscreen width but
+                        // apply ideal size height.
+                        yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f),
+                                idealSize.getHeight());
+                    }
+                    // For landscape resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                // If activity is unresizeable, regardless of orientation, calculate maximum size
+                // (within the ideal size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(
+                        activity.getOverrideOrientation(), idealSize, appAspectRatio);
+            }
+            case ORIENTATION_PORTRAIT -> {
+                // Device in portrait orientation.
+                final int customPortraitWidthForLandscapeApp = screenBounds.width()
+                        - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationLandscape(activityOrientation)) {
+                        if (appAspectRatio == 0) {
+                            appAspectRatio = tdaWidth / (tdaWidth - 1);
+                        }
+                        // For landscape resizeable activities, respect apps fullscreen height and
+                        // apply custom app width.
+                        yield new Size(customPortraitWidthForLandscapeApp,
+                                (int) ((tdaWidth / appAspectRatio) + 0.5f));
+                    }
+                    // For portrait resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (isFixedOrientationLandscape(activityOrientation)) {
+                    // For landscape unresizeable activities, apply custom app width to ideal size
+                    // and calculate maximum size with this area while maintaining original aspect
+                    // ratio.
+                    yield maximizeSizeGivenAspectRatio(activityOrientation,
+                            new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()),
+                            appAspectRatio);
+                }
+                // For portrait unresizeable activities, calculate maximum size (within the ideal
+                // size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+            }
+            default -> idealSize;
+        };
+        return centerInScreen(initialSize, screenBounds);
+    }
+
+    /**
+     * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+     * ratio.
+     */
+    private static @NonNull Size maximizeSizeGivenAspectRatio(
+            @ActivityInfo.ScreenOrientation int orientation,
+            @NonNull Size targetArea,
+            float aspectRatio
+    ) {
+        final int targetHeight = targetArea.getHeight();
+        final int targetWidth = targetArea.getWidth();
+        final int finalHeight;
+        final int finalWidth;
+        if (isFixedOrientationPortrait(orientation)) {
+            // Portrait activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight / aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth * aspectRatio);
+            }
+        } else {
+            // Landscape activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight * aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth / aspectRatio);
+            }
+        }
+        return new Size(finalWidth, finalHeight);
+    }
+
+    /**
+     * Calculates the aspect ratio of an activity from its fullscreen bounds.
+     */
+    @VisibleForTesting
+    static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        final float fullscreenWidth = task.getDisplayArea().getBounds().width();
+        final float fullscreenHeight = task.getDisplayArea().getBounds().height();
+        final float maxAspectRatio = activity.getMaxAspectRatio();
+        final float minAspectRatio = activity.getMinAspectRatio();
+        float desiredAspectRatio = 0;
+        if (taskInfo.isRunning) {
+            final AppCompatTaskInfo appCompatTaskInfo =  taskInfo.appCompatTaskInfo;
+            if (appCompatTaskInfo.topActivityBoundsLetterboxed) {
+                desiredAspectRatio = (float) Math.max(
+                        appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight)
+                        / Math.min(appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight);
+            } else {
+                desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth)
+                        / Math.min(fullscreenHeight, fullscreenWidth);
+            }
+        } else {
+            final float letterboxAspectRatioOverride =
+                    getFixedOrientationLetterboxAspectRatio(activity, task);
+            if (!task.mDisplayContent.getIgnoreOrientationRequest()) {
+                desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+            } else if (letterboxAspectRatioOverride
+                    > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+                desiredAspectRatio = letterboxAspectRatioOverride;
+            }
+        }
+        // If the activity matches display orientation, the display aspect ratio should be used
+        if (activityMatchesDisplayOrientation(
+                taskInfo.configuration.orientation,
+                activity.getOverrideOrientation())) {
+            desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight)
+                    / Math.min(fullscreenWidth, fullscreenHeight);
+        }
+        if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) {
+            desiredAspectRatio = maxAspectRatio;
+        } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) {
+            desiredAspectRatio = minAspectRatio;
+        }
+        return desiredAspectRatio;
+    }
+
+    private static boolean activityMatchesDisplayOrientation(
+            @Configuration.Orientation int deviceOrientation,
+            @ActivityInfo.ScreenOrientation int activityOrientation) {
+        if (deviceOrientation == ORIENTATION_PORTRAIT) {
+            return isFixedOrientationPortrait(activityOrientation);
+        }
+        return isFixedOrientationLandscape(activityOrientation);
+    }
+
+    /**
+     * Calculates the desired initial bounds for applications in desktop windowing. This is done as
+     * a scale of the screen bounds.
+     */
+    private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) {
+        final int width = (int) (screenBounds.width() * scale);
+        final int height = (int) (screenBounds.height() * scale);
+        return new Size(width, height);
+    }
+
+    /**
+     * Adjusts bounds to be positioned in the middle of the screen.
+     */
+    private static @NonNull Rect centerInScreen(@NonNull Size desiredSize,
             @NonNull Rect screenBounds) {
-        // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
-        // The desired dimensions that a fully resizable window should take when initially entering
-        // desktop mode. Calculated as a percentage of the available display area as defined by the
-        // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
-        final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        outBounds.right = desiredWidth;
-        outBounds.bottom = desiredHeight;
-        outBounds.offset(screenBounds.centerX() - outBounds.centerX(),
-                screenBounds.centerY() - outBounds.centerY());
+        // TODO(b/325240051): Position apps with bottom heavy offset
+        final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2;
+        final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2;
+        final Rect resultBounds = new Rect(0, 0,
+                desiredSize.getWidth(), desiredSize.getHeight());
+        resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset);
+        return resultBounds;
+    }
+
+    private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        return activity.shouldCreateCompatDisplayInsets()
+                ? getDefaultMinAspectRatioForUnresizableApps(activity, task)
+                : activity.mAppCompatController.getAppCompatAspectRatioOverrides()
+                        .getDefaultMinAspectRatio();
+    }
+
+    private static float getDefaultMinAspectRatioForUnresizableApps(
+            @NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides =
+                activity.mAppCompatController.getAppCompatAspectRatioOverrides();
+        if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) {
+            // Default letterbox aspect ratio for unresizable apps.
+            return getSplitScreenAspectRatio(activity, task);
+        }
+
+        if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig()
+                > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+            return appCompatAspectRatioOverrides
+                    .getDefaultMinAspectRatioForUnresizableAppsFromConfig();
+        }
+
+        return appCompatAspectRatioOverrides.getDefaultMinAspectRatio();
+    }
+
+    /**
+     * Calculates the aspect ratio of the available display area when an app enters split-screen on
+     * a given device, taking into account any dividers and insets.
+     */
+    private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final int dividerWindowWidth =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_thickness);
+        final int dividerInsets =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_insets);
+        final int dividerSize = dividerWindowWidth - dividerInsets * 2;
+        final Rect bounds = new Rect(0, 0,
+                task.mDisplayContent.getDisplayInfo().appWidth,
+                task.mDisplayContent.getDisplayInfo().appHeight);
+        if (bounds.width() >= bounds.height()) {
+            bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
+            bounds.right = bounds.centerX();
+        } else {
+            bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
+            bounds.bottom = bounds.centerY();
+        }
+        return computeAspectRatio(bounds);
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index 1f341147..e0c0c2c 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -22,7 +22,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.window.flags.Flags;
+import com.android.server.wm.utils.DesktopModeFlagsUtil;
 
 /**
  * Constants for desktop mode feature
@@ -35,8 +35,8 @@
             "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
 
     /** Whether desktop mode is enabled. */
-    static boolean isDesktopModeEnabled() {
-        return Flags.enableDesktopWindowingMode();
+    static boolean isDesktopModeEnabled(@NonNull Context context) {
+        return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context);
     }
 
     /**
@@ -60,7 +60,7 @@
      * Return {@code true} if desktop mode can be entered on the current device.
      */
     static boolean canEnterDesktopMode(@NonNull Context context) {
-        return isDesktopModeEnabled()
+        return isDesktopModeEnabled(context)
                 && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context));
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index aacd3c6..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -25,7 +25,6 @@
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.os.SystemProperties;
 import android.util.Slog;
 
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -38,19 +37,9 @@
             TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
     private static final boolean DEBUG = false;
 
-    public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
-            SystemProperties
-                    .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
-
-    /**
-     * Flag to indicate whether to restrict desktop mode to supported devices.
-     */
-    private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
-            "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
-
     private StringBuilder mLogBuilder;
 
-    private final Context mContext;
+    @NonNull private final Context mContext;
 
     DesktopModeLaunchParamsModifier(@NonNull Context context) {
         mContext = context;
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index d2976b0..8272e16 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -326,7 +326,7 @@
     DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy(
                 WindowManagerService service, DisplayContent displayContent) {
         return DisplayRotationImmersiveAppCompatPolicy.createIfNeeded(
-                service.mLetterboxConfiguration, this, displayContent);
+                service.mAppCompatConfiguration, this, displayContent);
     }
 
     // Change the default value to the value specified in the sysprop
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 1a0124a..63fe94c 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -247,7 +247,7 @@
      * </ul>
      */
     private boolean isTreatmentEnabledForDisplay() {
-        return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled()
+        return mWmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabled()
                 && mDisplayContent.getIgnoreOrientationRequest()
                 // TODO(b/225928882): Support camera compat rotation for external displays
                 && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
diff --git a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
index de70c4d..094434d 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java
@@ -40,28 +40,28 @@
 
     @Nullable
     static DisplayRotationImmersiveAppCompatPolicy createIfNeeded(
-            @NonNull final LetterboxConfiguration letterboxConfiguration,
+            @NonNull final AppCompatConfiguration appCompatConfiguration,
             @NonNull final DisplayRotation displayRotation,
             @NonNull final DisplayContent displayContent) {
-        if (!letterboxConfiguration
+        if (!appCompatConfiguration
                 .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime()) {
             return null;
         }
 
         return new DisplayRotationImmersiveAppCompatPolicy(
-                letterboxConfiguration, displayRotation, displayContent);
+                appCompatConfiguration, displayRotation, displayContent);
     }
 
     private final DisplayRotation mDisplayRotation;
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
     private final DisplayContent mDisplayContent;
 
     private DisplayRotationImmersiveAppCompatPolicy(
-            @NonNull final LetterboxConfiguration letterboxConfiguration,
+            @NonNull final AppCompatConfiguration appCompatConfiguration,
             @NonNull final DisplayRotation displayRotation,
             @NonNull final DisplayContent displayContent) {
         mDisplayRotation = displayRotation;
-        mLetterboxConfiguration = letterboxConfiguration;
+        mAppCompatConfiguration = appCompatConfiguration;
         mDisplayContent = displayContent;
     }
 
@@ -80,14 +80,14 @@
      *   <li>Rotation will lead to letterboxing due to fixed orientation.
      *   <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true}
      *   <li>This policy is enabled on the device, for details see
-     *   {@link LetterboxConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled}
+     *   {@link AppCompatConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled}
      * </ul>
      *
      * @param proposedRotation new proposed {@link Surface.Rotation} for the screen.
      * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise.
      */
     boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) {
-        if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) {
+        if (!mAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) {
             return false;
         }
         synchronized (mDisplayContent.mWmService.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3483842..dcadb0f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -397,9 +397,11 @@
                 onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
             }
             newControlTargets.clear();
-            // Check for and try to run the scheduled show IME request (if it exists), as we
-            // now applied the surface transaction and notified the target of the new control.
-            getImeSourceProvider().checkAndStartShowImePostLayout();
+            if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+                // Check for and try to run the scheduled show IME request (if it exists), as we
+                // now applied the surface transaction and notified the target of the new control.
+                getImeSourceProvider().checkAndStartShowImePostLayout();
+            }
         });
     }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 235d6cd..be8e806 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -37,17 +37,17 @@
 import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
-import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -67,7 +67,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.LetterboxDetails;
-import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
 import com.android.window.flags.Flags;
 
 import java.io.PrintWriter;
@@ -81,7 +81,7 @@
 
     private final Point mTmpPoint = new Point();
 
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
 
     private final ActivityRecord mActivityRecord;
 
@@ -95,7 +95,7 @@
     private boolean mDoubleTapEvent;
 
     LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
-        mLetterboxConfiguration = wmService.mLetterboxConfiguration;
+        mAppCompatConfiguration = wmService.mAppCompatConfiguration;
         // Given activityRecord may not be fully constructed since LetterboxUiController
         // is created in its constructor. It shouldn't be used in this constructor but it's safe
         // to use it after since controller is only used in ActivityRecord.
@@ -345,7 +345,7 @@
     private boolean shouldLetterboxHaveRoundedCorners() {
         // TODO(b/214030873): remove once background is drawn for transparent activities
         // Letterbox shouldn't have rounded corners if the activity is transparent
-        return mLetterboxConfiguration.isLetterboxActivityCornersRounded()
+        return mAppCompatConfiguration.isLetterboxActivityCornersRounded()
                 && mActivityRecord.fillsParent();
     }
 
@@ -382,13 +382,13 @@
         return isHorizontalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
-                ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
-                : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
+                ? mAppCompatConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled)
+                : mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled);
     }
 
     private boolean isFullScreenAndBookModeEnabled() {
         return isDisplayFullScreenAndInPosture(/* isTabletop */ false)
-                && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
+                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
     }
 
     float getVerticalPositionMultiplier(Configuration parentConfiguration) {
@@ -398,8 +398,8 @@
         return isVerticalReachabilityEnabled(parentConfiguration)
                 // Using the last global dynamic position to avoid "jumps" when moving
                 // between apps or activities.
-                ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
-                : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
+                ? mAppCompatConfiguration.getVerticalMultiplierForReachability(tabletopMode)
+                : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
     }
 
     float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) {
@@ -408,14 +408,14 @@
     }
 
     boolean isLetterboxEducationEnabled() {
-        return mLetterboxConfiguration.getIsEducationEnabled();
+        return mAppCompatConfiguration.getIsEducationEnabled();
     }
 
     /**
      * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
      */
     boolean isVerticalThinLetterboxed() {
-        final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx();
+        final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx();
         if (thinHeight < 0) {
             return false;
         }
@@ -432,7 +432,7 @@
      * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
      */
     boolean isHorizontalThinLetterboxed() {
-        final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx();
+        final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx();
         if (thinWidth < 0) {
             return false;
         }
@@ -477,17 +477,17 @@
                 .shouldOverrideMinAspectRatio();
     }
 
-    @LetterboxConfiguration.LetterboxVerticalReachabilityPosition
+    @AppCompatConfiguration.LetterboxVerticalReachabilityPosition
     int getLetterboxPositionForVerticalReachability() {
         final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
-        return mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(
+        return mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(
                 isInFullScreenTabletopMode);
     }
 
-    @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition
+    @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability() {
         final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled();
-        return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(
+        return mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(
                 isInFullScreenBookMode);
     }
 
@@ -503,12 +503,12 @@
         }
 
         boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge()
-                && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
-        int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
+                && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
+        int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
                 .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
         if (mLetterbox.getInnerFrame().left > x) {
             // Moving to the next stop on the left side of the app window: right > center > left.
-            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
+            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
                     isInFullScreenBookMode);
             int changeToLog =
                     letterboxPositionForHorizontalReachability
@@ -519,7 +519,7 @@
             mDoubleTapEvent = true;
         } else if (mLetterbox.getInnerFrame().right < x) {
             // Moving to the next stop on the right side of the app window: left > center > right.
-            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
                     isInFullScreenBookMode);
             int changeToLog =
                     letterboxPositionForHorizontalReachability
@@ -544,11 +544,11 @@
             return;
         }
         boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
-        int letterboxPositionForVerticalReachability = mLetterboxConfiguration
+        int letterboxPositionForVerticalReachability = mAppCompatConfiguration
                 .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
         if (mLetterbox.getInnerFrame().top > y) {
             // Moving to the next stop on the top side of the app window: bottom > center > top.
-            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
+            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextTopStop(
                     isInFullScreenTabletopMode);
             int changeToLog =
                     letterboxPositionForVerticalReachability
@@ -559,7 +559,7 @@
             mDoubleTapEvent = true;
         } else if (mLetterbox.getInnerFrame().bottom < y) {
             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
-            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
                     isInFullScreenTabletopMode);
             int changeToLog =
                     letterboxPositionForVerticalReachability
@@ -596,7 +596,7 @@
                 .getTransparentPolicy().getFirstOpaqueActivity()
                 .map(ActivityRecord::getScreenResolvedBounds)
                 .orElse(mActivityRecord.getScreenResolvedBounds());
-        return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()
+        return mAppCompatConfiguration.getIsHorizontalReachabilityEnabled()
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 // Check whether the activity fills the parent vertically.
@@ -641,7 +641,7 @@
                 .getTransparentPolicy().getFirstOpaqueActivity()
                 .map(ActivityRecord::getScreenResolvedBounds)
                 .orElse(mActivityRecord.getScreenResolvedBounds());
-        return mLetterboxConfiguration.getIsVerticalReachabilityEnabled()
+        return mAppCompatConfiguration.getIsVerticalReachabilityEnabled()
                 && parentConfiguration.windowConfiguration.getWindowingMode()
                         == WINDOWING_MODE_FULLSCREEN
                 // Check whether the activity fills the parent horizontally.
@@ -681,7 +681,7 @@
             return Color.valueOf(Color.BLACK);
         }
         @LetterboxBackgroundType int letterboxBackgroundType =
-                mLetterboxConfiguration.getLetterboxBackgroundType();
+                mAppCompatConfiguration.getLetterboxBackgroundType();
         TaskDescription taskDescription = mActivityRecord.taskDescription;
         switch (letterboxBackgroundType) {
             case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
@@ -697,7 +697,7 @@
             case LETTERBOX_BACKGROUND_WALLPAPER:
                 if (hasWallpaperBackgroundForLetterbox()) {
                     // Color is used for translucent scrim that dims wallpaper.
-                    return mLetterboxConfiguration.getLetterboxBackgroundColor();
+                    return mAppCompatConfiguration.getLetterboxBackgroundColor();
                 }
                 Slog.w(TAG, "Wallpaper option is selected for letterbox background but "
                         + "blur is not supported by a device or not supported in the current "
@@ -705,14 +705,14 @@
                         + "provided so using solid color background");
                 break;
             case LETTERBOX_BACKGROUND_SOLID_COLOR:
-                return mLetterboxConfiguration.getLetterboxBackgroundColor();
+                return mAppCompatConfiguration.getLetterboxBackgroundColor();
             default:
                 throw new AssertionError(
                     "Unexpected letterbox background type: " + letterboxBackgroundType);
         }
         // If picked option configured incorrectly or not supported then default to a solid color
         // background.
-        return mLetterboxConfiguration.getLetterboxBackgroundColor();
+        return mAppCompatConfiguration.getLetterboxBackgroundColor();
     }
 
     private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
@@ -771,7 +771,7 @@
 
     private boolean requiresRoundedCorners(final WindowState mainWindow) {
         return isLetterboxedNotForDisplayCutout(mainWindow)
-                && mLetterboxConfiguration.isLetterboxActivityCornersRounded();
+                && mAppCompatConfiguration.isLetterboxActivityCornersRounded();
     }
 
     // Returns rounded corners radius the letterboxed activity should have based on override in
@@ -785,8 +785,8 @@
         }
 
         final int radius;
-        if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
-            radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius();
+        if (mAppCompatConfiguration.getLetterboxActivityCornersRadius() >= 0) {
+            radius = mAppCompatConfiguration.getLetterboxActivityCornersRadius();
         } else {
             final InsetsState insetsState = mainWindow.getInsetsState();
             radius = Math.min(
@@ -850,7 +850,7 @@
 
     private void updateWallpaperForLetterbox(WindowState mainWindow) {
         @LetterboxBackgroundType int letterboxBackgroundType =
-                mLetterboxConfiguration.getLetterboxBackgroundType();
+                mAppCompatConfiguration.getLetterboxBackgroundType();
         boolean wallpaperShouldBeShown =
                 letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
                         // Don't use wallpaper as a background if letterboxed for display cutout.
@@ -868,18 +868,18 @@
     }
 
     private int getLetterboxWallpaperBlurRadiusPx() {
-        int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
+        int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
         return Math.max(blurRadius, 0);
     }
 
     private float getLetterboxWallpaperDarkScrimAlpha() {
-        float alpha = mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
+        float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
         // No scrim by default.
         return (alpha < 0 || alpha >= 1) ? 0.0f : alpha;
     }
 
     private boolean isLetterboxWallpaperBlurSupported() {
-        return mLetterboxConfiguration.mContext.getSystemService(WindowManager.class)
+        return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class)
                 .isCrossWindowBlurEnabled();
     }
 
@@ -911,10 +911,10 @@
                 getLetterboxBackgroundColor().toArgb()));
         pw.println(prefix + "  letterboxBackgroundType="
                 + letterboxBackgroundTypeToString(
-                        mLetterboxConfiguration.getLetterboxBackgroundType()));
+                        mAppCompatConfiguration.getLetterboxBackgroundType()));
         pw.println(prefix + "  letterboxCornerRadius="
                 + getRoundedCornersRadius(mainWin));
-        if (mLetterboxConfiguration.getLetterboxBackgroundType()
+        if (mAppCompatConfiguration.getLetterboxBackgroundType()
                 == LETTERBOX_BACKGROUND_WALLPAPER) {
             pw.println(prefix + "  isLetterboxWallpaperBlurSupported="
                     + isLetterboxWallpaperBlurSupported());
@@ -932,19 +932,19 @@
         pw.println(prefix + "  letterboxVerticalPositionMultiplier="
                 + getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
         pw.println(prefix + "  letterboxPositionForHorizontalReachability="
-                + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
-                mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
+                + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString(
+                mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(false)));
         pw.println(prefix + "  letterboxPositionForVerticalReachability="
-                + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
-                mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
+                + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString(
+                mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(false)));
         pw.println(prefix + "  fixedOrientationLetterboxAspectRatio="
-                + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
+                + mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio());
         pw.println(prefix + "  defaultMinAspectRatioForUnresizableApps="
-                + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
+                + mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps());
         pw.println(prefix + "  isSplitScreenAspectRatioForUnresizableAppsEnabled="
-                + mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+                + mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
         pw.println(prefix + "  isDisplayAspectRatioEnabledForFixedOrientationLetterbox="
-                + mLetterboxConfiguration
+                + mAppCompatConfiguration
                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
     }
 
@@ -971,7 +971,7 @@
     }
 
     private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
-            @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) {
+            @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position) {
         switch (position) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
@@ -987,7 +987,7 @@
     }
 
     private int letterboxVerticalReachabilityPositionToLetterboxPosition(
-            @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) {
+            @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int position) {
         switch (position) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
                 return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
@@ -1005,13 +1005,13 @@
     int getLetterboxPositionForLogging() {
         int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
         if (isHorizontalReachabilityEnabled()) {
-            int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
+            int letterboxPositionForHorizontalReachability = mAppCompatConfiguration
                     .getLetterboxPositionForHorizontalReachability(
                             isDisplayFullScreenAndInPosture(/* isTabletop */ false));
             positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
                     letterboxPositionForHorizontalReachability);
         } else if (isVerticalReachabilityEnabled()) {
-            int letterboxPositionForVerticalReachability = mLetterboxConfiguration
+            int letterboxPositionForVerticalReachability = mAppCompatConfiguration
                     .getLetterboxPositionForVerticalReachability(
                             isDisplayFullScreenAndInPosture(/* isTabletop */ true));
             positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index cff40c7..a1e6701 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -146,7 +146,10 @@
                     + activity);
             return null;
         }
-        final WindowState mainWindow = activity.findMainWindow(false);
+        // For snapshot surface, the top activity could be trampoline activity, so here should
+        // search for top fullscreen activity in the task.
+        final WindowState mainWindow = task
+                .getTopFullscreenMainWindow(false /* includeStartingApp */);
         if (mainWindow == null) {
             Slog.w(TAG, "TaskSnapshotSurface.create: no main window in " + activity);
             return null;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 74f8a068..8f83a7c 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3007,11 +3007,17 @@
         return r.getTask().mTaskId != taskId && r.token != notTop && r.canBeTopRunning();
     }
 
-    ActivityRecord getTopFullscreenActivity() {
-        return getActivity((r) -> {
-            final WindowState win = r.findMainWindow();
-            return (win != null && win.mAttrs.isFullscreen());
+    WindowState getTopFullscreenMainWindow(boolean includeStartingApp) {
+        final WindowState[] candidate = new WindowState[1];
+        getActivity((r) -> {
+            final WindowState win = r.findMainWindow(includeStartingApp);
+            if (win != null && win.mAttrs.isFullscreen()) {
+                candidate[0] = win;
+                return true;
+            }
+            return false;
         });
+        return candidate[0];
     }
 
     /**
@@ -3377,7 +3383,7 @@
         // Whether the direct top activity is in size compat mode
         appCompatTaskInfo.topActivityInSizeCompat = isTopActivityVisible && top.inSizeCompatMode();
         if (appCompatTaskInfo.topActivityInSizeCompat
-                && mWmService.mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) {
+                && mWmService.mAppCompatConfiguration.isTranslucentLetterboxingEnabled()) {
             // We hide the restart button in case of transparent activities.
             appCompatTaskInfo.topActivityInSizeCompat = top.fillsParent();
         }
@@ -3579,14 +3585,12 @@
         // starting window because persisted configuration does not effect to Task.
         info.taskInfo.configuration.setTo(activity.getConfiguration());
         if (!Flags.drawSnapshotAspectRatioMatch()) {
-            final ActivityRecord topFullscreenActivity = getTopFullscreenActivity();
-            if (topFullscreenActivity != null) {
-                final WindowState mainWindow = topFullscreenActivity.findMainWindow(false);
-                if (mainWindow != null) {
-                    info.topOpaqueWindowInsetsState =
-                            mainWindow.getInsetsStateWithVisibilityOverride();
-                    info.topOpaqueWindowLayoutParams = mainWindow.getAttrs();
-                }
+            final WindowState mainWindow =
+                    getTopFullscreenMainWindow(false /* includeStartingApp */);
+            if (mainWindow != null) {
+                info.topOpaqueWindowInsetsState =
+                        mainWindow.getInsetsStateWithVisibilityOverride();
+                info.topOpaqueWindowLayoutParams = mainWindow.getAttrs();
             }
         }
         return info;
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 4218f8f..57c7753 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -261,8 +261,8 @@
     }
 
     @Override
-    ActivityRecord getTopFullscreenActivity(Task source) {
-        return source.getTopFullscreenActivity();
+    WindowState getTopFullscreenWindow(Task source) {
+        return source.getTopFullscreenMainWindow(true /* includeStartingApp */);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index f6a68d5..72092e5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1766,7 +1766,19 @@
         }
 
         for (int i = 0; i < mTargets.size(); ++i) {
-            final DisplayArea da = mTargets.get(i).mContainer.asDisplayArea();
+            final WindowContainer<?> wc = mTargets.get(i).mContainer;
+            final WallpaperWindowToken wp = wc.asWallpaperToken();
+            if (wp != null) {
+                // If on a rotation leash, the wallpaper token surface needs to be shown explicitly
+                // because shell only gets the leash and the wallpaper token surface is not allowed
+                // to be changed by non-transition logic until the transition is finished.
+                if (Flags.ensureWallpaperInTransitions() && wp.isVisibleRequested()
+                        && wp.getFixedRotationLeash() != null) {
+                    transaction.show(wp.mSurfaceControl);
+                }
+                continue;
+            }
+            final DisplayArea<?> da = wc.asDisplayArea();
             if (da == null) continue;
             if (da.isVisibleRequested()) {
                 mController.mValidateDisplayVis.remove(da);
@@ -2168,14 +2180,6 @@
                         && !wallpaperIsOwnTarget(wallpaper)) {
                     wallpaper.setVisibleRequested(false);
                 }
-                if (showWallpaper && Flags.ensureWallpaperInTransitions()
-                        && wallpaper.isVisibleRequested()
-                        && getLeashSurface(wallpaper, t) != wallpaper.getSurfaceControl()) {
-                    // If on a rotation leash, we need to explicitly show the wallpaper surface
-                    // because shell only gets the leash and we don't allow non-transition logic
-                    // to touch the surfaces until the transition is over.
-                    t.show(wallpaper.getSurfaceControl());
-                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
index cdb14ab..2f46103 100644
--- a/services/core/java/com/android/server/wm/TransparentPolicy.java
+++ b/services/core/java/com/android/server/wm/TransparentPolicy.java
@@ -73,10 +73,10 @@
     private final TransparentPolicyState mTransparentPolicyState;
 
     TransparentPolicy(@NonNull ActivityRecord activityRecord,
-            @NonNull LetterboxConfiguration letterboxConfiguration) {
+            @NonNull AppCompatConfiguration appCompatConfiguration) {
         mActivityRecord = activityRecord;
         mIsTranslucentLetterboxingEnabledSupplier =
-                letterboxConfiguration::isTranslucentLetterboxingEnabled;
+                appCompatConfiguration::isTranslucentLetterboxingEnabled;
         mTransparentPolicyState = new TransparentPolicyState(activityRecord);
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8299c33..30ea086 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -120,10 +120,10 @@
 import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
 import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
 import static com.android.server.wm.SensitiveContentPackages.PackageInfo;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
@@ -1054,7 +1054,7 @@
     private boolean mAnimationsDisabled = false;
     boolean mPointerLocationEnabled = false;
 
-    final LetterboxConfiguration mLetterboxConfiguration;
+    final AppCompatConfiguration mAppCompatConfiguration;
 
     private boolean mIsIgnoreOrientationRequestDisabled;
 
@@ -1293,7 +1293,7 @@
                     | WindowInsets.Type.navigationBars();
         }
 
-        mLetterboxConfiguration = new LetterboxConfiguration(
+        mAppCompatConfiguration = new AppCompatConfiguration(
                 // Using SysUI context to have access to Material colors extracted from Wallpaper.
                 ActivityThread.currentActivityThread().getSystemUiContext());
 
@@ -4439,7 +4439,7 @@
      */
     boolean isIgnoreOrientationRequestDisabled() {
         return mIsIgnoreOrientationRequestDisabled
-                || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed();
+                || !mAppCompatConfiguration.isIgnoreOrientationRequestAllowed();
     }
 
     @Override
@@ -9990,7 +9990,7 @@
      */
     @Override
     public int getLetterboxBackgroundColorInArgb() {
-        return mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb();
+        return mAppCompatConfiguration.getLetterboxBackgroundColor().toArgb();
     }
 
     /**
@@ -9998,8 +9998,8 @@
      */
     @Override
     public boolean isLetterboxBackgroundMultiColored() {
-        @LetterboxConfiguration.LetterboxBackgroundType int letterboxBackgroundType =
-                mLetterboxConfiguration.getLetterboxBackgroundType();
+        @AppCompatConfiguration.LetterboxBackgroundType int letterboxBackgroundType =
+                mAppCompatConfiguration.getLetterboxBackgroundType();
         switch (letterboxBackgroundType) {
             case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
             case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND:
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 6febe80..51d5bc0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -19,16 +19,16 @@
 import static android.os.Build.IS_USER;
 import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
 
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 
 import android.content.res.Resources.NotFoundException;
 import android.graphics.Color;
@@ -50,12 +50,12 @@
 import com.android.internal.os.ByteTransferPipe;
 import com.android.internal.protolog.LegacyProtoLogImpl;
 import com.android.internal.protolog.PerfettoProtoLogImpl;
-import com.android.internal.protolog.common.IProtoLog;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLog;
 import com.android.server.IoThread;
-import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
-import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
-import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition;
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+import com.android.server.wm.AppCompatConfiguration.LetterboxHorizontalReachabilityPosition;
+import com.android.server.wm.AppCompatConfiguration.LetterboxVerticalReachabilityPosition;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -78,12 +78,12 @@
 
     // Internal service impl -- must perform security checks before touching.
     private final WindowManagerService mInternal;
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
 
     public WindowManagerShellCommand(WindowManagerService service) {
         mInterface = service;
         mInternal = service;
-        mLetterboxConfiguration = service.mLetterboxConfiguration;
+        mAppCompatConfiguration = service.mAppCompatConfiguration;
     }
 
     @Override
@@ -678,7 +678,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
+            mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
         }
         return 0;
     }
@@ -698,7 +698,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio);
+            mAppCompatConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio);
         }
         return 0;
     }
@@ -717,7 +717,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
+            mAppCompatConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
         }
         return 0;
     }
@@ -752,7 +752,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundTypeOverride(backgroundType);
+            mAppCompatConfiguration.setLetterboxBackgroundTypeOverride(backgroundType);
         }
         return 0;
     }
@@ -770,7 +770,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId);
+            mAppCompatConfiguration.setLetterboxBackgroundColorResourceId(colorId);
         }
         return 0;
     }
@@ -787,7 +787,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundColor(color);
+            mAppCompatConfiguration.setLetterboxBackgroundColor(color);
         }
         return 0;
     }
@@ -809,7 +809,7 @@
         synchronized (mInternal.mGlobalLock) {
             final int radiusPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                     radiusDp, mInternal.mContext.getResources().getDisplayMetrics());
-            mLetterboxConfiguration.setLetterboxBackgroundWallpaperBlurRadiusPx(radiusPx);
+            mAppCompatConfiguration.setLetterboxBackgroundWallpaperBlurRadiusPx(radiusPx);
         }
         return 0;
     }
@@ -829,7 +829,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
+            mAppCompatConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
         }
         return 0;
     }
@@ -849,7 +849,7 @@
         }
         synchronized (mInternal.mGlobalLock) {
             try {
-                mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
+                mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
             } catch (IllegalArgumentException  e) {
                 getErrPrintWriter().println("Error: invalid multiplier value " + e);
                 return -1;
@@ -873,7 +873,7 @@
         }
         synchronized (mInternal.mGlobalLock) {
             try {
-                mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier);
+                mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(multiplier);
             } catch (IllegalArgumentException  e) {
                 getErrPrintWriter().println("Error: invalid multiplier value " + e);
                 return -1;
@@ -908,7 +908,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setDefaultPositionForHorizontalReachability(position);
+            mAppCompatConfiguration.setDefaultPositionForHorizontalReachability(position);
         }
         return 0;
     }
@@ -939,7 +939,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setDefaultPositionForVerticalReachability(position);
+            mAppCompatConfiguration.setDefaultPositionForVerticalReachability(position);
         }
         return 0;
     }
@@ -970,7 +970,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
+            mAppCompatConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
                     false /* IsInBookMode */, position);
         }
         return 0;
@@ -1002,7 +1002,7 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setPersistentLetterboxPositionForVerticalReachability(
+            mAppCompatConfiguration.setPersistentLetterboxPositionForVerticalReachability(
                     false /* forTabletopMode */, position);
         }
         return 0;
@@ -1074,15 +1074,15 @@
                     runSetLetterboxVerticalPositionMultiplier(pw);
                     break;
                 case "--isHorizontalReachabilityEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setIsHorizontalReachabilityEnabled);
                     break;
                 case "--isVerticalReachabilityEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setIsVerticalReachabilityEnabled);
                     break;
                 case "--isAutomaticReachabilityInBookModeEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setIsAutomaticReachabilityInBookModeEnabled);
                     break;
                 case "--defaultPositionForHorizontalReachability":
@@ -1098,34 +1098,34 @@
                     runSetPersistentLetterboxPositionForVerticalReachability(pw);
                     break;
                 case "--isEducationEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled);
+                    runSetBooleanFlag(pw, mAppCompatConfiguration::setIsEducationEnabled);
                     break;
                 case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
                     break;
                 case "--isDisplayAspectRatioEnabledForFixedOrientationLetterbox":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox);
                     break;
                 case "--isTranslucentLetterboxingEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setTranslucentLetterboxingOverrideEnabled);
                     break;
                 case "--isUserAppAspectRatioSettingsEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setUserAppAspectRatioSettingsOverrideEnabled);
                     break;
                 case "--isUserAppAspectRatioFullscreenEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration
+                    runSetBooleanFlag(pw, mAppCompatConfiguration
                             ::setUserAppAspectRatioFullscreenOverrideEnabled);
                     break;
                 case "--isCameraCompatRefreshEnabled":
-                    runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled);
+                    runSetBooleanFlag(pw, mAppCompatConfiguration::setCameraCompatRefreshEnabled);
                     break;
                 case "--isCameraCompatRefreshCycleThroughStopEnabled":
                     runSetBooleanFlag(pw,
-                            mLetterboxConfiguration::setCameraCompatRefreshCycleThroughStopEnabled);
+                            mAppCompatConfiguration::setCameraCompatRefreshCycleThroughStopEnabled);
                     break;
                 default:
                     getErrPrintWriter().println(
@@ -1145,77 +1145,77 @@
                 String arg = getNextArg();
                 switch (arg) {
                     case "aspectRatio":
-                        mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
+                        mAppCompatConfiguration.resetFixedOrientationLetterboxAspectRatio();
                         break;
                     case "minAspectRatioForUnresizable":
-                        mLetterboxConfiguration.resetDefaultMinAspectRatioForUnresizableApps();
+                        mAppCompatConfiguration.resetDefaultMinAspectRatioForUnresizableApps();
                         break;
                     case "cornerRadius":
-                        mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
+                        mAppCompatConfiguration.resetLetterboxActivityCornersRadius();
                         break;
                     case "backgroundType":
-                        mLetterboxConfiguration.resetLetterboxBackgroundType();
+                        mAppCompatConfiguration.resetLetterboxBackgroundType();
                         break;
                     case "backgroundColor":
-                        mLetterboxConfiguration.resetLetterboxBackgroundColor();
+                        mAppCompatConfiguration.resetLetterboxBackgroundColor();
                         break;
                     case "wallpaperBlurRadius":
-                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx();
+                        mAppCompatConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx();
                         break;
                     case "wallpaperDarkScrimAlpha":
-                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
+                        mAppCompatConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
                         break;
                     case "horizontalPositionMultiplier":
-                        mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
+                        mAppCompatConfiguration.resetLetterboxHorizontalPositionMultiplier();
                         break;
                     case "verticalPositionMultiplier":
-                        mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
+                        mAppCompatConfiguration.resetLetterboxVerticalPositionMultiplier();
                         break;
                     case "isHorizontalReachabilityEnabled":
-                        mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
+                        mAppCompatConfiguration.resetIsHorizontalReachabilityEnabled();
                         break;
                     case "isVerticalReachabilityEnabled":
-                        mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
+                        mAppCompatConfiguration.resetIsVerticalReachabilityEnabled();
                         break;
                     case "defaultPositionForHorizontalReachability":
-                        mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
+                        mAppCompatConfiguration.resetDefaultPositionForHorizontalReachability();
                         break;
                     case "defaultPositionForVerticalReachability":
-                        mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
+                        mAppCompatConfiguration.resetDefaultPositionForVerticalReachability();
                         break;
                     case "persistentPositionForHorizontalReachability":
-                        mLetterboxConfiguration
+                        mAppCompatConfiguration
                                 .resetPersistentLetterboxPositionForHorizontalReachability();
                         break;
                     case "persistentPositionForVerticalReachability":
-                        mLetterboxConfiguration
+                        mAppCompatConfiguration
                                 .resetPersistentLetterboxPositionForVerticalReachability();
                         break;
                     case "isEducationEnabled":
-                        mLetterboxConfiguration.resetIsEducationEnabled();
+                        mAppCompatConfiguration.resetIsEducationEnabled();
                         break;
                     case "isSplitScreenAspectRatioForUnresizableAppsEnabled":
-                        mLetterboxConfiguration
+                        mAppCompatConfiguration
                                 .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
                         break;
                     case "IsDisplayAspectRatioEnabledForFixedOrientationLetterbox":
-                        mLetterboxConfiguration
+                        mAppCompatConfiguration
                                 .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
                         break;
                     case "isTranslucentLetterboxingEnabled":
-                        mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+                        mAppCompatConfiguration.resetTranslucentLetterboxingEnabled();
                         break;
                     case "isUserAppAspectRatioSettingsEnabled":
-                        mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
+                        mAppCompatConfiguration.resetUserAppAspectRatioSettingsEnabled();
                         break;
                     case "isUserAppAspectRatioFullscreenEnabled":
-                        mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
+                        mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled();
                         break;
                     case "isCameraCompatRefreshEnabled":
-                        mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+                        mAppCompatConfiguration.resetCameraCompatRefreshEnabled();
                         break;
                     case "isCameraCompatRefreshCycleThroughStopEnabled":
-                        mLetterboxConfiguration
+                        mAppCompatConfiguration
                                 .resetCameraCompatRefreshCycleThroughStopEnabled();
                         break;
                     default:
@@ -1304,104 +1304,104 @@
 
     private void resetLetterboxStyle() {
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
-            mLetterboxConfiguration.resetDefaultMinAspectRatioForUnresizableApps();
-            mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
-            mLetterboxConfiguration.resetLetterboxBackgroundType();
-            mLetterboxConfiguration.resetLetterboxBackgroundColor();
-            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx();
-            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
-            mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
-            mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
-            mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
-            mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
-            mLetterboxConfiguration.resetEnabledAutomaticReachabilityInBookMode();
-            mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
-            mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
-            mLetterboxConfiguration.resetPersistentLetterboxPositionForHorizontalReachability();
-            mLetterboxConfiguration.resetPersistentLetterboxPositionForVerticalReachability();
-            mLetterboxConfiguration.resetIsEducationEnabled();
-            mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
-            mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
-            mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
-            mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
-            mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
-            mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
-            mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
+            mAppCompatConfiguration.resetFixedOrientationLetterboxAspectRatio();
+            mAppCompatConfiguration.resetDefaultMinAspectRatioForUnresizableApps();
+            mAppCompatConfiguration.resetLetterboxActivityCornersRadius();
+            mAppCompatConfiguration.resetLetterboxBackgroundType();
+            mAppCompatConfiguration.resetLetterboxBackgroundColor();
+            mAppCompatConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx();
+            mAppCompatConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
+            mAppCompatConfiguration.resetLetterboxHorizontalPositionMultiplier();
+            mAppCompatConfiguration.resetLetterboxVerticalPositionMultiplier();
+            mAppCompatConfiguration.resetIsHorizontalReachabilityEnabled();
+            mAppCompatConfiguration.resetIsVerticalReachabilityEnabled();
+            mAppCompatConfiguration.resetEnabledAutomaticReachabilityInBookMode();
+            mAppCompatConfiguration.resetDefaultPositionForHorizontalReachability();
+            mAppCompatConfiguration.resetDefaultPositionForVerticalReachability();
+            mAppCompatConfiguration.resetPersistentLetterboxPositionForHorizontalReachability();
+            mAppCompatConfiguration.resetPersistentLetterboxPositionForVerticalReachability();
+            mAppCompatConfiguration.resetIsEducationEnabled();
+            mAppCompatConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+            mAppCompatConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+            mAppCompatConfiguration.resetTranslucentLetterboxingEnabled();
+            mAppCompatConfiguration.resetUserAppAspectRatioSettingsEnabled();
+            mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled();
+            mAppCompatConfiguration.resetCameraCompatRefreshEnabled();
+            mAppCompatConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
         }
     }
 
     private int runGetLetterboxStyle(PrintWriter pw) throws RemoteException {
         synchronized (mInternal.mGlobalLock) {
             pw.println("Corner radius: "
-                    + mLetterboxConfiguration.getLetterboxActivityCornersRadius());
+                    + mAppCompatConfiguration.getLetterboxActivityCornersRadius());
             pw.println("Horizontal position multiplier: "
-                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+                    + mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(
                             false /* isInBookMode */));
             pw.println("Vertical position multiplier: "
-                    + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+                    + mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(
                             false /* isInTabletopMode */));
             pw.println("Horizontal position multiplier (book mode): "
-                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+                    + mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(
                             true /* isInBookMode */));
             pw.println("Vertical position multiplier (tabletop mode): "
-                    + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+                    + mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(
                             true /* isInTabletopMode */));
             pw.println("Horizontal position multiplier for reachability: "
-                    + mLetterboxConfiguration.getHorizontalMultiplierForReachability(
+                    + mAppCompatConfiguration.getHorizontalMultiplierForReachability(
                             false /* isInBookMode */));
             pw.println("Vertical position multiplier for reachability: "
-                    + mLetterboxConfiguration.getVerticalMultiplierForReachability(
+                    + mAppCompatConfiguration.getVerticalMultiplierForReachability(
                             false /* isInTabletopMode */));
             pw.println("Aspect ratio: "
-                    + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
+                    + mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio());
             pw.println("Default min aspect ratio for unresizable apps: "
-                    + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps());
+                    + mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps());
             pw.println("Is horizontal reachability enabled: "
-                    + mLetterboxConfiguration.getIsHorizontalReachabilityEnabled());
+                    + mAppCompatConfiguration.getIsHorizontalReachabilityEnabled());
             pw.println("Is vertical reachability enabled: "
-                    + mLetterboxConfiguration.getIsVerticalReachabilityEnabled());
+                    + mAppCompatConfiguration.getIsVerticalReachabilityEnabled());
             pw.println("Is automatic reachability in book mode enabled: "
-                    + mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled());
+                    + mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled());
             pw.println("Default position for horizontal reachability: "
-                    + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
-                            mLetterboxConfiguration.getDefaultPositionForHorizontalReachability()));
+                    + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString(
+                            mAppCompatConfiguration.getDefaultPositionForHorizontalReachability()));
             pw.println("Default position for vertical reachability: "
-                    + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
-                    mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
+                    + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString(
+                    mAppCompatConfiguration.getDefaultPositionForVerticalReachability()));
             pw.println("Current position for horizontal reachability:"
-                    + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
-                    mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
+                    + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString(
+                    mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(false)));
             pw.println("Current position for vertical reachability:"
-                    + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
-                    mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
+                    + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString(
+                    mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(false)));
             pw.println("Is education enabled: "
-                    + mLetterboxConfiguration.getIsEducationEnabled());
+                    + mAppCompatConfiguration.getIsEducationEnabled());
             pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
-                    + mLetterboxConfiguration
+                    + mAppCompatConfiguration
                             .getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
             pw.println("Is using display aspect ratio as aspect ratio for all letterboxed apps: "
-                    + mLetterboxConfiguration
+                    + mAppCompatConfiguration
                             .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
             pw.println("    Is activity \"refresh\" in camera compatibility treatment enabled: "
-                    + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
+                    + mAppCompatConfiguration.isCameraCompatRefreshEnabled());
             pw.println("    Refresh using \"stopped -> resumed\" cycle: "
-                    + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
+                    + mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
             pw.println("Background type: "
-                    + LetterboxConfiguration.letterboxBackgroundTypeToString(
-                            mLetterboxConfiguration.getLetterboxBackgroundType()));
+                    + AppCompatConfiguration.letterboxBackgroundTypeToString(
+                            mAppCompatConfiguration.getLetterboxBackgroundType()));
             pw.println("    Background color: " + Integer.toHexString(
-                    mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb()));
+                    mAppCompatConfiguration.getLetterboxBackgroundColor().toArgb()));
             pw.println("    Wallpaper blur radius: "
-                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx());
+                    + mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx());
             pw.println("    Wallpaper dark scrim alpha: "
-                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
+                    + mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
             pw.println("Is letterboxing for translucent activities enabled: "
-                    + mLetterboxConfiguration.isTranslucentLetterboxingEnabled());
+                    + mAppCompatConfiguration.isTranslucentLetterboxingEnabled());
             pw.println("Is the user aspect ratio settings enabled: "
-                    + mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled());
+                    + mAppCompatConfiguration.isUserAppAspectRatioSettingsEnabled());
             pw.println("Is the fullscreen option in user aspect ratio settings enabled: "
-                    + mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled());
+                    + mAppCompatConfiguration.isUserAppAspectRatioFullscreenEnabled());
         }
         return 0;
     }
@@ -1539,13 +1539,13 @@
         pw.println("    Sets letterbox style using the following options:");
         pw.println("      --aspectRatio aspectRatio");
         pw.println("        Aspect ratio of letterbox for fixed orientation. If aspectRatio <= "
-                + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+                + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
         pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
         pw.println("        be ignored and framework implementation will determine aspect ratio.");
         pw.println("      --minAspectRatioForUnresizable aspectRatio");
         pw.println("        Default min aspect ratio for unresizable apps which is used when an");
         pw.println("        app is eligible for the size compat mode.  If aspectRatio <= "
-                + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+                + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
         pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
         pw.println("        be ignored and framework implementation will determine aspect ratio.");
         pw.println("      --cornerRadius radius");
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 60d3e78..12c5073 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY;
 import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED;
 import static android.os.Build.VERSION_CODES.Q;
@@ -73,6 +74,7 @@
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -112,6 +114,13 @@
     private static final String TAG_RELEASE = TAG + POSTFIX_RELEASE;
     private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION;
 
+    /**
+     * The max number of processes which can be top scheduling group if there are non-top visible
+     * freeform activities run in the process.
+     */
+    private static final int MAX_NUM_PERCEPTIBLE_FREEFORM =
+            SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1);
+
     private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200;
     private static final long RAPID_ACTIVITY_LAUNCH_MS = 500;
     private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 3 * RAPID_ACTIVITY_LAUNCH_MS;
@@ -318,6 +327,7 @@
     public static final int ACTIVITY_STATE_FLAG_HAS_RESUMED = 1 << 21;
     public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22;
     public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23;
+    public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24;
     public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff;
 
     /**
@@ -1229,6 +1239,7 @@
         ActivityRecord.State bestInvisibleState = DESTROYED;
         boolean allStoppingFinishing = true;
         boolean visible = false;
+        boolean hasResumedFreeform = false;
         int minTaskLayer = Integer.MAX_VALUE;
         int stateFlags = 0;
         final boolean wasResumed = hasResumedActivity();
@@ -1256,6 +1267,8 @@
                                     .processPriorityPolicyForMultiWindowMode()
                             && task.getAdjacentTask() != null) {
                         stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN;
+                    } else if (windowingMode == WINDOWING_MODE_FREEFORM) {
+                        hasResumedFreeform = true;
                     }
                 }
                 if (minTaskLayer > 0) {
@@ -1289,6 +1302,12 @@
             }
         }
 
+        if (hasResumedFreeform
+                && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()
+                // Exclude task layer 1 because it is already the top most.
+                && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) {
+            stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM;
+        }
         stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER;
         if (visible) {
             stateFlags |= ACTIVITY_STATE_FLAG_IS_VISIBLE;
@@ -2105,6 +2124,9 @@
                     if ((stateFlags & ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN) != 0) {
                         pw.print("RS|");
                     }
+                    if ((stateFlags & ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) {
+                        pw.print("PF|");
+                    }
                 }
             } else if ((stateFlags & ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED) != 0) {
                 pw.print("P|");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 9ebb89d..a36cff6 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4648,14 +4648,16 @@
         if (!isImeLayeringTarget()) {
             return false;
         }
-        // Note that we don't process IME window if the IME input target is not on the screen.
-        // In case some unexpected IME visibility cases happen like starting the remote
-        // animation on the keyguard but seeing the IME window that originally on the app
-        // which behinds the keyguard.
-        final WindowState imeInputTarget = getImeInputTarget();
-        if (imeInputTarget != null
-                && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) {
-            return false;
+        if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) {
+            // Note that we don't process IME window if the IME input target is not on the screen.
+            // In case some unexpected IME visibility cases happen like starting the remote
+            // animation on the keyguard but seeing the IME window that originally on the app
+            // which behinds the keyguard.
+            final WindowState imeInputTarget = getImeInputTarget();
+            if (imeInputTarget != null
+                    && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) {
+                return false;
+            }
         }
         return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom);
     }
@@ -5504,7 +5506,8 @@
 
     @Override
     public SurfaceControl getAnimationLeashParent() {
-        if (isStartingWindowAssociatedToTask()) {
+        if (mActivityRecord != null && !mActivityRecord.hasFixedRotationTransform()
+                && isStartingWindowAssociatedToTask()) {
             return mStartingData.mAssociatedTask.mSurfaceControl;
         }
         return super.getAnimationLeashParent();
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index 21f7eca..04d5c03 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -56,7 +56,10 @@
 
     static WindowTracing createDefaultAndStartLooper(WindowManagerService service,
             Choreographer choreographer) {
-        return new WindowTracingLegacy(service, choreographer);
+        if (!android.tracing.Flags.perfettoWmTracing()) {
+            return new WindowTracingLegacy(service, choreographer);
+        }
+        return new WindowTracingPerfetto(service, choreographer);
     }
 
     protected WindowTracing(WindowManagerService service, Choreographer choreographer,
diff --git a/services/core/java/com/android/server/wm/WindowTracingDataSource.java b/services/core/java/com/android/server/wm/WindowTracingDataSource.java
new file mode 100644
index 0000000..3d2c0d3
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowTracingDataSource.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT;
+
+import android.annotation.NonNull;
+import android.internal.perfetto.protos.DataSourceConfigOuterClass.DataSourceConfig;
+import android.internal.perfetto.protos.WindowmanagerConfig.WindowManagerConfig;
+import android.tracing.perfetto.CreateTlsStateArgs;
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+public final class WindowTracingDataSource extends DataSource<WindowTracingDataSource.Instance,
+        WindowTracingDataSource.TlsState, Void> {
+    public static final String DATA_SOURCE_NAME = "android.windowmanager";
+
+    public static class TlsState {
+        public final Config mConfig;
+        public final AtomicBoolean mIsStarting = new AtomicBoolean(true);
+
+        private TlsState(Config config) {
+            mConfig = config;
+        }
+    }
+
+    public static class Config {
+        public final @WindowTraceLogLevel int mLogLevel;
+        public final boolean mLogOnFrame;
+
+        private Config(@WindowTraceLogLevel int logLevel, boolean logOnFrame) {
+            mLogLevel = logLevel;
+            mLogOnFrame = logOnFrame;
+        }
+    }
+
+    public abstract static class Instance extends DataSourceInstance {
+        public final Config mConfig;
+
+        public Instance(DataSource dataSource, int instanceIndex, Config config) {
+            super(dataSource, instanceIndex);
+            mConfig = config;
+        }
+    }
+
+    private static final Config CONFIG_DEFAULT = new Config(WindowTraceLogLevel.TRIM, true);
+    private static final int CONFIG_VALUE_UNSPECIFIED = 0;
+    private static final String TAG = "WindowTracingDataSource";
+
+    @NonNull
+    private final Consumer<Config> mOnStartCallback;
+    @NonNull
+    private final Consumer<Config> mOnStopCallback;
+
+    public WindowTracingDataSource(@NonNull Consumer<Config> onStart,
+            @NonNull Consumer<Config> onStop) {
+        super(DATA_SOURCE_NAME);
+        mOnStartCallback = onStart;
+        mOnStopCallback = onStop;
+
+        Producer.init(InitArguments.DEFAULTS);
+        DataSourceParams params =
+                new DataSourceParams.Builder()
+                        .setBufferExhaustedPolicy(
+                                PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT)
+                        .build();
+        register(params);
+    }
+
+    @Override
+    public Instance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        final Config config = parseDataSourceConfig(configStream);
+
+        return new Instance(this, instanceIndex, config != null ? config : CONFIG_DEFAULT) {
+            @Override
+            protected void onStart(StartCallbackArguments args) {
+                mOnStartCallback.accept(mConfig);
+            }
+
+            @Override
+            protected void onStop(StopCallbackArguments args) {
+                mOnStopCallback.accept(mConfig);
+            }
+        };
+    }
+
+    @Override
+    public TlsState createTlsState(
+            CreateTlsStateArgs<Instance> args) {
+        try (Instance dsInstance = args.getDataSourceInstanceLocked()) {
+            if (dsInstance == null) {
+                // Datasource instance has been removed
+                return new TlsState(CONFIG_DEFAULT);
+            }
+            return new TlsState(dsInstance.mConfig);
+        }
+    }
+
+    private Config parseDataSourceConfig(ProtoInputStream stream) {
+        try {
+            while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                if (stream.getFieldNumber() != (int) DataSourceConfig.WINDOWMANAGER_CONFIG) {
+                    continue;
+                }
+                return parseWindowManagerConfig(stream);
+            }
+            Log.w(TAG, "Received start request without config parameters. Will use defaults.");
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to parse DataSourceConfig", e);
+        }
+        return null;
+    }
+
+    private Config parseWindowManagerConfig(ProtoInputStream stream) {
+        int parsedLogLevel = CONFIG_VALUE_UNSPECIFIED;
+        int parsedLogFrequency = CONFIG_VALUE_UNSPECIFIED;
+
+        try {
+            final long token = stream.start(DataSourceConfig.WINDOWMANAGER_CONFIG);
+            while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                switch (stream.getFieldNumber()) {
+                    case (int) WindowManagerConfig.LOG_LEVEL:
+                        parsedLogLevel = stream.readInt(WindowManagerConfig.LOG_LEVEL);
+                        break;
+                    case (int) WindowManagerConfig.LOG_FREQUENCY:
+                        parsedLogFrequency = stream.readInt(WindowManagerConfig.LOG_FREQUENCY);
+                        break;
+                    default:
+                        Log.w(TAG, "Unrecognized WindowManagerConfig field number: "
+                                + stream.getFieldNumber());
+                }
+            }
+            stream.end(token);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to parse WindowManagerConfig", e);
+        }
+
+        @WindowTraceLogLevel int logLevel;
+        switch(parsedLogLevel) {
+            case CONFIG_VALUE_UNSPECIFIED:
+                Log.w(TAG, "Unspecified log level. Defaulting to TRIM");
+                logLevel = WindowTraceLogLevel.TRIM;
+                break;
+            case WindowManagerConfig.LOG_LEVEL_VERBOSE:
+                logLevel = WindowTraceLogLevel.ALL;
+                break;
+            case WindowManagerConfig.LOG_LEVEL_DEBUG:
+                logLevel = WindowTraceLogLevel.TRIM;
+                break;
+            case WindowManagerConfig.LOG_LEVEL_CRITICAL:
+                logLevel = WindowTraceLogLevel.CRITICAL;
+                break;
+            default:
+                Log.w(TAG, "Unrecognized log level. Defaulting to TRIM");
+                logLevel = WindowTraceLogLevel.TRIM;
+                break;
+        }
+
+        boolean logOnFrame;
+        switch(parsedLogFrequency) {
+            case CONFIG_VALUE_UNSPECIFIED:
+                Log.w(TAG, "Unspecified log frequency. Defaulting to 'log on frame'");
+                logOnFrame = true;
+                break;
+            case WindowManagerConfig.LOG_FREQUENCY_FRAME:
+                logOnFrame = true;
+                break;
+            case WindowManagerConfig.LOG_FREQUENCY_TRANSACTION:
+                logOnFrame = false;
+                break;
+            default:
+                Log.w(TAG, "Unrecognized log frequency. Defaulting to 'log on frame'");
+                logOnFrame = true;
+                break;
+        }
+
+        return new Config(logLevel, logOnFrame);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java
new file mode 100644
index 0000000..653b6da
--- /dev/null
+++ b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java
@@ -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.server.wm;
+
+import android.annotation.Nullable;
+import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket;
+import android.internal.perfetto.protos.WinscopeExtensionsImplOuterClass.WinscopeExtensionsImpl;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.Choreographer;
+
+import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class WindowTracingPerfetto extends WindowTracing {
+    private static final String TAG = "WindowTracing";
+
+    private final AtomicInteger mCountSessionsOnFrame = new AtomicInteger();
+    private final AtomicInteger mCountSessionsOnTransaction = new AtomicInteger();
+    private final WindowTracingDataSource mDataSource = new WindowTracingDataSource(
+            this::onStart, this::onStop);
+
+    WindowTracingPerfetto(WindowManagerService service, Choreographer choreographer) {
+        super(service, choreographer, service.mGlobalLock);
+    }
+
+    @Override
+    void setLogLevel(@WindowTraceLogLevel int logLevel, PrintWriter pw) {
+        logAndPrintln(pw, "Log level must be configured through perfetto");
+    }
+
+    @Override
+    void setLogFrequency(boolean onFrame, PrintWriter pw) {
+        logAndPrintln(pw, "Log frequency must be configured through perfetto");
+    }
+
+    @Override
+    void setBufferCapacity(int capacity, PrintWriter pw) {
+        logAndPrintln(pw, "Buffer capacity must be configured through perfetto");
+    }
+
+    @Override
+    boolean isEnabled() {
+        return (mCountSessionsOnFrame.get() + mCountSessionsOnTransaction.get()) > 0;
+    }
+
+    @Override
+    int onShellCommand(ShellCommand shell) {
+        PrintWriter pw = shell.getOutPrintWriter();
+        pw.println("Shell commands are ignored."
+                + " Any type of action should be performed through perfetto.");
+        return -1;
+    }
+
+    @Override
+    String getStatus() {
+        return "Status: "
+                + ((isEnabled()) ? "Enabled" : "Disabled")
+                + "\n"
+                + "Sessions logging 'on frame': " + mCountSessionsOnFrame.get()
+                + "\n"
+                + "Sessions logging 'on transaction': " + mCountSessionsOnTransaction.get()
+                + "\n";
+    }
+
+    @Override
+    protected void startTraceInternal(@Nullable PrintWriter pw) {
+        logAndPrintln(pw, "Tracing must be started through perfetto");
+    }
+
+    @Override
+    protected void stopTraceInternal(@Nullable PrintWriter pw) {
+        logAndPrintln(pw, "Tracing must be stopped through perfetto");
+    }
+
+    @Override
+    protected void saveForBugreportInternal(@Nullable PrintWriter pw) {
+        logAndPrintln(pw, "Tracing snapshot for bugreport must be handled through perfetto");
+    }
+
+    @Override
+    protected void log(String where) {
+        try {
+            boolean isStartLogEvent = where == WHERE_START_TRACING;
+            boolean isOnFrameLogEvent = where == WHERE_ON_FRAME;
+
+            mDataSource.trace((context) -> {
+                WindowTracingDataSource.Config dataSourceConfig =
+                        context.getCustomTlsState().mConfig;
+
+                if (isStartLogEvent) {
+                    boolean isDataSourceStarting = context.getCustomTlsState()
+                            .mIsStarting.compareAndSet(true, false);
+                    if (!isDataSourceStarting) {
+                        return;
+                    }
+                } else if (isOnFrameLogEvent != dataSourceConfig.mLogOnFrame) {
+                    return;
+                }
+
+                ProtoOutputStream os = context.newTracePacket();
+                long timestamp = SystemClock.elapsedRealtimeNanos();
+                os.write(TracePacket.TIMESTAMP, timestamp);
+                final long tokenWinscopeExtensions =
+                        os.start(TracePacket.WINSCOPE_EXTENSIONS);
+                final long tokenExtensionsField =
+                        os.start(WinscopeExtensionsImpl.WINDOWMANAGER);
+                dumpToProto(os, dataSourceConfig.mLogLevel, where, timestamp);
+                os.end(tokenExtensionsField);
+                os.end(tokenWinscopeExtensions);
+            });
+        } catch (Exception e) {
+            Log.wtf(TAG, "Exception while tracing state", e);
+        }
+    }
+
+    @Override
+    protected boolean shouldLogOnFrame() {
+        return mCountSessionsOnFrame.get() > 0;
+    }
+
+    @Override
+    protected boolean shouldLogOnTransaction() {
+        return mCountSessionsOnTransaction.get() > 0;
+    }
+
+    private void onStart(WindowTracingDataSource.Config config) {
+        if (config.mLogOnFrame) {
+            mCountSessionsOnFrame.incrementAndGet();
+        } else {
+            mCountSessionsOnTransaction.incrementAndGet();
+        }
+
+        Log.i(TAG, "Started with logLevel: " + config.mLogLevel
+                + " logOnFrame: " + config.mLogOnFrame);
+        log(WHERE_START_TRACING);
+    }
+
+    private void onStop(WindowTracingDataSource.Config config) {
+        if (config.mLogOnFrame) {
+            mCountSessionsOnFrame.decrementAndGet();
+        } else {
+            mCountSessionsOnTransaction.decrementAndGet();
+        }
+        Log.i(TAG, "Stopped");
+    }
+}
diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
new file mode 100644
index 0000000..4211764
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.window.flags.Flags;
+
+import java.util.function.Supplier;
+
+/**
+ * Util to check desktop mode flags state.
+ *
+ * This utility is used to allow developer option toggles to override flags related to desktop
+ * windowing.
+ *
+ * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag
+ * value and the developer option override state (if applicable).
+ *
+ * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which
+ * is to be used in WM core.
+ */
+public enum DesktopModeFlagsUtil {
+    // All desktop mode related flags to be overridden by developer option toggle will be added here
+    DESKTOP_WINDOWING_MODE(
+            Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
+    WALLPAPER_ACTIVITY(
+            Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true);
+
+    private static final String TAG = "DesktopModeFlagsUtil";
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    // Function called to obtain aconfig flag value.
+    private final Supplier<Boolean> mFlagFunction;
+    // Whether the flag state should be affected by developer option.
+    private final boolean mShouldOverrideByDevOption;
+
+    // Local cache for toggle override, which is initialized once on its first access. It needs to
+    // be refreshed only on reboots as overridden state takes effect on reboots.
+    private static ToggleOverride sCachedToggleOverride;
+
+    DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) {
+        this.mFlagFunction = flagFunction;
+        this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+    }
+
+    /**
+     * Determines state of flag based on the actual flag and desktop mode developer option
+     * overrides.
+     *
+     * Note: this method makes sure that a constant developer toggle overrides is read until
+     * reboot.
+     */
+    public boolean isEnabled(Context context) {
+        if (!Flags.showDesktopWindowingDevOption()
+                || !mShouldOverrideByDevOption
+                || context.getContentResolver() == null) {
+            return mFlagFunction.get();
+        } else {
+            boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
+            return switch (getToggleOverride(context)) {
+                case OVERRIDE_UNSET -> mFlagFunction.get();
+                // When toggle override matches its default state, don't override flags. This
+                // helps users reset their feature overrides.
+                case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get();
+                case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true;
+            };
+        }
+    }
+
+    private ToggleOverride getToggleOverride(Context context) {
+        // If cached, return it
+        if (sCachedToggleOverride != null) {
+            return sCachedToggleOverride;
+        }
+
+        // Otherwise, fetch and cache it
+        ToggleOverride override = getToggleOverrideFromSystem(context);
+        sCachedToggleOverride = override;
+        Log.d(TAG, "Toggle override initialized to: " + override);
+        return override;
+    }
+
+    /**
+     *  Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise
+     *  initializes the system property by reading Settings.Global.
+     */
+    private ToggleOverride getToggleOverrideFromSystem(Context context) {
+        // A non-persistent System Property is used to store override to ensure it remains
+        // constant till reboot.
+        String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null);
+        ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty);
+
+        // If valid system property, return it
+        if (overrideFromSystemProperties != null) {
+            return overrideFromSystemProperties;
+        }
+
+        // Fallback when System Property is not present (just after reboot) or not valid (user
+        // manually changed the value): Read from Settings.Global
+        int settingValue = Settings.Global.getInt(
+                context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+                OVERRIDE_UNSET.getSetting()
+        );
+        ToggleOverride overrideFromSettingsGlobal =
+                ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET);
+        // Initialize System Property
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue));
+        return overrideFromSettingsGlobal;
+    }
+
+    /**
+     * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if
+     * {@code intString} does not correspond to a {@link ToggleOverride}.
+     */
+    private static @Nullable ToggleOverride convertToToggleOverride(
+            @Nullable String intString
+    ) {
+        if (intString == null) return null;
+        try {
+            int intValue = Integer.parseInt(intString);
+            return ToggleOverride.fromSetting(intValue, null);
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "Unknown toggleOverride int " + intString);
+            return null;
+        }
+    }
+
+    /** Override state of desktop mode developer option toggle. */
+    enum ToggleOverride {
+        OVERRIDE_UNSET,
+        OVERRIDE_OFF,
+        OVERRIDE_ON;
+
+        int getSetting() {
+            return switch (this) {
+                case OVERRIDE_ON -> 1;
+                case OVERRIDE_OFF -> 0;
+                case OVERRIDE_UNSET -> -1;
+            };
+        }
+
+        static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) {
+            return switch (setting) {
+                case 1 -> OVERRIDE_ON;
+                case 0 -> OVERRIDE_OFF;
+                case -1 -> OVERRIDE_UNSET;
+                default -> fallback;
+            };
+        }
+    }
+}
diff --git a/services/core/jni/com_android_server_UsbDeviceManager.cpp b/services/core/jni/com_android_server_UsbDeviceManager.cpp
index 9dc70af..4ef9cf4 100644
--- a/services/core/jni/com_android_server_UsbDeviceManager.cpp
+++ b/services/core/jni/com_android_server_UsbDeviceManager.cpp
@@ -15,33 +15,168 @@
  */
 
 #define LOG_TAG "UsbDeviceManagerJNI"
-#include "utils/Log.h"
-
-#include "jni.h"
+#include <android-base/properties.h>
+#include <android-base/unique_fd.h>
+#include <core_jni_helpers.h>
+#include <fcntl.h>
+#include <linux/usb/f_accessory.h>
 #include <nativehelper/JNIPlatformHelp.h>
 #include <nativehelper/ScopedUtfChars.h>
+#include <stdio.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <thread>
+
+#include "MtpDescriptors.h"
 #include "android_runtime/AndroidRuntime.h"
 #include "android_runtime/Log.h"
-#include "MtpDescriptors.h"
-
-#include <stdio.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <sys/ioctl.h>
-#include <linux/usb/f_accessory.h>
+#include "jni.h"
+#include "utils/Log.h"
 
 #define DRIVER_NAME "/dev/usb_accessory"
+#define EPOLL_MAX_EVENTS 4
+#define USB_STATE_MAX_LEN 20
 
 namespace android
 {
 
+static JavaVM *gvm = nullptr;
+static jmethodID gUpdateGadgetStateMethod;
+
 static struct parcel_file_descriptor_offsets_t
 {
     jclass mClass;
     jmethodID mConstructor;
 } gParcelFileDescriptorOffsets;
 
+/*
+ * NativeGadgetMonitorThread starts a new thread to monitor udc state by epoll,
+ * convert and update the state to UsbDeviceManager.
+ */
+class NativeGadgetMonitorThread {
+    android::base::unique_fd mMonitorFd;
+    int mPipefd[2];
+    std::thread mThread;
+    jobject mCallbackObj;
+    std::string mGadgetState;
+
+    void handleStateUpdate(const char *state) {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+        std::string gadgetState;
+
+        if (!std::strcmp(state, "not attached\n")) {
+            gadgetState = "DISCONNECTED";
+        } else if (!std::strcmp(state, "attached\n") || !std::strcmp(state, "powered\n") ||
+                   !std::strcmp(state, "default\n") || !std::strcmp(state, "addressed\n")) {
+            gadgetState = "CONNECTED";
+        } else if (!std::strcmp(state, "configured\n")) {
+            gadgetState = "CONFIGURED";
+        } else if (!std::strcmp(state, "suspended\n")) {
+            return;
+        } else {
+            ALOGE("Unknown gadget state %s", state);
+            return;
+        }
+
+        if (mGadgetState.compare(gadgetState)) {
+            mGadgetState = gadgetState;
+            jstring obj = env->NewStringUTF(gadgetState.c_str());
+            env->CallVoidMethod(mCallbackObj, gUpdateGadgetStateMethod, obj);
+        }
+    }
+
+    int setupEpoll(android::base::unique_fd &epollFd) {
+        struct epoll_event ev;
+
+        ev.data.fd = mMonitorFd.get();
+        ev.events = EPOLLPRI;
+        if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mMonitorFd.get(), &ev) != 0) {
+            ALOGE("epoll_ctl failed for monitor fd; errno=%d", errno);
+            return errno;
+        }
+
+        ev.data.fd = mPipefd[0];
+        ev.events = EPOLLIN;
+        if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mPipefd[0], &ev) != 0) {
+            ALOGE("epoll_ctl failed for pipe fd; errno=%d", errno);
+            return errno;
+        }
+
+        return 0;
+    }
+
+    void monitorLoop() {
+        android::base::unique_fd epollFd(epoll_create(EPOLL_MAX_EVENTS));
+        if (epollFd.get() == -1) {
+            ALOGE("epoll_create failed; errno=%d", errno);
+            return;
+        }
+        if (setupEpoll(epollFd) != 0) return;
+
+        JNIEnv *env = nullptr;
+        JavaVMAttachArgs aargs = {JNI_VERSION_1_4, "NativeGadgetMonitorThread", nullptr};
+        if (gvm->AttachCurrentThread(&env, &aargs) != JNI_OK || env == nullptr) {
+            ALOGE("Couldn't attach thread");
+            return;
+        }
+
+        struct epoll_event events[EPOLL_MAX_EVENTS];
+        int nevents = 0;
+        while (true) {
+            nevents = epoll_wait(epollFd.get(), events, EPOLL_MAX_EVENTS, -1);
+            if (nevents < 0) {
+                ALOGE("usb epoll_wait failed; errno=%d", errno);
+                continue;
+            }
+            for (int i = 0; i < nevents; ++i) {
+                int fd = events[i].data.fd;
+                if (fd == mPipefd[0]) {
+                    goto exit;
+                } else if (fd == mMonitorFd.get()) {
+                    char state[USB_STATE_MAX_LEN] = {0};
+                    lseek(fd, 0, SEEK_SET);
+                    read(fd, &state, USB_STATE_MAX_LEN);
+                    handleStateUpdate(state);
+                }
+            }
+        }
+
+    exit:
+        auto res = gvm->DetachCurrentThread();
+        ALOGE_IF(res != JNI_OK, "Couldn't detach thread");
+        return;
+    }
+
+    void stop() {
+        if (mThread.joinable()) {
+            int c = 'q';
+            write(mPipefd[1], &c, 1);
+            mThread.join();
+        }
+    }
+
+    DISALLOW_COPY_AND_ASSIGN(NativeGadgetMonitorThread);
+
+public:
+    explicit NativeGadgetMonitorThread(jobject obj, android::base::unique_fd monitorFd)
+          : mMonitorFd(std::move(monitorFd)), mGadgetState("") {
+        mCallbackObj = AndroidRuntime::getJNIEnv()->NewGlobalRef(obj);
+        pipe(mPipefd);
+        mThread = std::thread(&NativeGadgetMonitorThread::monitorLoop, this);
+    }
+
+    ~NativeGadgetMonitorThread() {
+        stop();
+        close(mPipefd[0]);
+        close(mPipefd[1]);
+        AndroidRuntime::getJNIEnv()->DeleteGlobalRef(mCallbackObj);
+    }
+};
+static std::unique_ptr<NativeGadgetMonitorThread> sGadgetMonitorThread;
+
 static void set_accessory_string(JNIEnv *env, int fd, int cmd, jobjectArray strArray, int index)
 {
     char buffer[256];
@@ -135,6 +270,41 @@
     return jifd;
 }
 
+static jboolean android_server_UsbDeviceManager_startGadgetMonitor(JNIEnv *env, jobject thiz,
+                                                                   jstring jUdcName) {
+    std::string filePath;
+    ScopedUtfChars udcName(env, jUdcName);
+
+    filePath = "/sys/class/udc/" + std::string(udcName.c_str()) + "/state";
+    android::base::unique_fd fd(open(filePath.c_str(), O_RDONLY));
+
+    if (fd.get() == -1) {
+        ALOGE("Cannot open %s", filePath.c_str());
+        return JNI_FALSE;
+    }
+
+    ALOGI("Start monitoring %s", filePath.c_str());
+    sGadgetMonitorThread.reset(new NativeGadgetMonitorThread(thiz, std::move(fd)));
+
+    return JNI_TRUE;
+}
+
+static void android_server_UsbDeviceManager_stopGadgetMonitor(JNIEnv *env, jobject /* thiz */) {
+    sGadgetMonitorThread.reset();
+    return;
+}
+
+static jstring android_server_UsbDeviceManager_waitAndGetProperty(JNIEnv *env, jobject thiz,
+                                                                  jstring jPropName) {
+    ScopedUtfChars propName(env, jPropName);
+    std::string propValue;
+
+    while (!android::base::WaitForPropertyCreation(propName.c_str()));
+    propValue = android::base::GetProperty(propName.c_str(), "" /* default */);
+
+    return env->NewStringUTF(propValue.c_str());
+}
+
 static const JNINativeMethod method_table[] = {
         {"nativeGetAccessoryStrings", "()[Ljava/lang/String;",
          (void *)android_server_UsbDeviceManager_getAccessoryStrings},
@@ -143,16 +313,26 @@
         {"nativeIsStartRequested", "()Z", (void *)android_server_UsbDeviceManager_isStartRequested},
         {"nativeOpenControl", "(Ljava/lang/String;)Ljava/io/FileDescriptor;",
          (void *)android_server_UsbDeviceManager_openControl},
+        {"nativeStartGadgetMonitor", "(Ljava/lang/String;)Z",
+         (void *)android_server_UsbDeviceManager_startGadgetMonitor},
+        {"nativeStopGadgetMonitor", "()V",
+         (void *)android_server_UsbDeviceManager_stopGadgetMonitor},
+        {"nativeWaitAndGetProperty", "(Ljava/lang/String;)Ljava/lang/String;",
+         (void *)android_server_UsbDeviceManager_waitAndGetProperty},
 };
 
-int register_android_server_UsbDeviceManager(JNIEnv *env)
-{
+int register_android_server_UsbDeviceManager(JavaVM *vm, JNIEnv *env) {
+    gvm = vm;
+
     jclass clazz = env->FindClass("com/android/server/usb/UsbDeviceManager");
     if (clazz == NULL) {
         ALOGE("Can't find com/android/server/usb/UsbDeviceManager");
         return -1;
     }
 
+    gUpdateGadgetStateMethod =
+            GetMethodIDOrDie(env, clazz, "updateGadgetState", "(Ljava/lang/String;)V");
+
     clazz = env->FindClass("android/os/ParcelFileDescriptor");
     LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.ParcelFileDescriptor");
     gParcelFileDescriptorOffsets.mClass = (jclass) env->NewGlobalRef(clazz);
@@ -163,5 +343,4 @@
     return jniRegisterNativeMethods(env, "com/android/server/usb/UsbDeviceManager",
             method_table, NELEM(method_table));
 }
-
 };
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 5719810..4d6a90c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -104,7 +104,6 @@
 
 static struct {
     jclass clazz;
-    jmethodID notifyConfigurationChanged;
     jmethodID notifyInputDevicesChanged;
     jmethodID notifySwitch;
     jmethodID notifyInputChannelBroken;
@@ -314,7 +313,6 @@
 
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
     void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override;
-    void notifyConfigurationChanged(nsecs_t when) override;
     std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay(
             const InputDeviceIdentifier& identifier,
             const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override;
@@ -940,18 +938,6 @@
     checkAndClearExceptionFromCallback(env, "notifySwitch");
 }
 
-void NativeInputManager::notifyConfigurationChanged(nsecs_t when) {
-#if DEBUG_INPUT_DISPATCHER_POLICY
-    ALOGD("notifyConfigurationChanged - when=%lld", when);
-#endif
-    ATRACE_CALL();
-
-    JNIEnv* env = jniEnv();
-
-    env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyConfigurationChanged, when);
-    checkAndClearExceptionFromCallback(env, "notifyConfigurationChanged");
-}
-
 static jobject getInputApplicationHandleObjLocalRef(
         JNIEnv* env, const std::shared_ptr<InputApplicationHandle>& inputApplicationHandle) {
     if (inputApplicationHandle == nullptr) {
@@ -2873,9 +2859,6 @@
     FIND_CLASS(clazz, "com/android/server/input/InputManagerService");
     gServiceClassInfo.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(clazz));
 
-    GET_METHOD_ID(gServiceClassInfo.notifyConfigurationChanged, clazz,
-            "notifyConfigurationChanged", "(J)V");
-
     GET_METHOD_ID(gServiceClassInfo.notifyInputDevicesChanged, clazz,
             "notifyInputDevicesChanged", "([Landroid/view/InputDevice;)V");
 
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 6464081..314ff9d 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -37,7 +37,7 @@
 int register_android_server_SystemServer(JNIEnv* env);
 int register_android_server_UsbAlsaJackDetector(JNIEnv* env);
 int register_android_server_UsbAlsaMidiDevice(JNIEnv* env);
-int register_android_server_UsbDeviceManager(JNIEnv* env);
+int register_android_server_UsbDeviceManager(JavaVM* vm, JNIEnv* env);
 int register_android_server_UsbHostManager(JNIEnv* env);
 int register_android_server_vr_VrManagerService(JNIEnv* env);
 int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env);
@@ -96,7 +96,7 @@
     register_android_server_SerialService(env);
     register_android_server_InputManager(env);
     register_android_server_LightsService(env);
-    register_android_server_UsbDeviceManager(env);
+    register_android_server_UsbDeviceManager(vm, env);
     register_android_server_UsbAlsaJackDetector(env);
     register_android_server_UsbAlsaMidiDevice(env);
     register_android_server_UsbHostManager(env);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index dc8cec9..6a0dd5a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -182,6 +182,7 @@
     private static final String TAG_CREDENTIAL_MANAGER_POLICY = "credential-manager-policy";
     private static final String TAG_DIALER_PACKAGE = "dialer_package";
     private static final String TAG_SMS_PACKAGE = "sms_package";
+    private static final String TAG_PROVISIONING_CONTEXT = "provisioning-context";
 
     // If the ActiveAdmin is a permission-based admin, then info will be null because the
     // permission-based admin is not mapped to a device administrator component.
@@ -359,6 +360,8 @@
     int mWifiMinimumSecurityLevel = DevicePolicyManager.WIFI_SECURITY_OPEN;
     String mDialerPackage;
     String mSmsPackage;
+    private String mProvisioningContext;
+    private static final int PROVISIONING_CONTEXT_LENGTH_LIMIT = 1000;
 
     ActiveAdmin(DeviceAdminInfo info, boolean isParent) {
         this.userId = -1;
@@ -404,6 +407,23 @@
         return UserHandle.of(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid));
     }
 
+    /**
+     * Stores metadata about context of setting an active admin
+     * @param provisioningContext some metadata, for example test method name
+     */
+    public void setProvisioningContext(@Nullable String provisioningContext) {
+        if (Flags.provisioningContextParameter()
+                && !TextUtils.isEmpty(provisioningContext)
+                && !provisioningContext.isBlank()) {
+            if (provisioningContext.length() > PROVISIONING_CONTEXT_LENGTH_LIMIT) {
+                mProvisioningContext = provisioningContext.substring(
+                        0, PROVISIONING_CONTEXT_LENGTH_LIMIT);
+            } else {
+                mProvisioningContext = provisioningContext;
+            }
+        }
+    }
+
     void writeToXml(TypedXmlSerializer out)
             throws IllegalArgumentException, IllegalStateException, IOException {
         if (info != null) {
@@ -694,6 +714,12 @@
         if (!TextUtils.isEmpty(mSmsPackage)) {
             writeAttributeValueToXml(out, TAG_SMS_PACKAGE, mSmsPackage);
         }
+
+        if (Flags.provisioningContextParameter() && !TextUtils.isEmpty(mProvisioningContext)) {
+            out.startTag(null, TAG_PROVISIONING_CONTEXT);
+            out.attribute(null, ATTR_VALUE, mProvisioningContext);
+            out.endTag(null, TAG_PROVISIONING_CONTEXT);
+        }
     }
 
     private void writePackagePolicy(TypedXmlSerializer out, String tag,
@@ -1006,6 +1032,9 @@
                 mDialerPackage = parser.getAttributeValue(null, ATTR_VALUE);
             } else if (TAG_SMS_PACKAGE.equals(tag)) {
                 mSmsPackage = parser.getAttributeValue(null, ATTR_VALUE);
+            } else if (Flags.provisioningContextParameter()
+                    && TAG_PROVISIONING_CONTEXT.equals(tag)) {
+                mProvisioningContext = parser.getAttributeValue(null, ATTR_VALUE);
             } else {
                 Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag);
                 XmlUtils.skipCurrentTag(parser);
@@ -1496,5 +1525,10 @@
         pw.println(mDialerPackage);
         pw.print("mSmsPackage=");
         pw.println(mSmsPackage);
+
+        if (Flags.provisioningContextParameter()) {
+            pw.print("mProvisioningContext=");
+            pw.println(mProvisioningContext);
+        }
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 032d6b5..7cb8ace 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1378,7 +1378,10 @@
 
         // Clear always-on configuration if it wasn't set by the admin.
         if (adminConfiguredVpnPkg == null) {
-            mInjector.getVpnManager().setAlwaysOnVpnPackageForUser(userId, null, false, null);
+            VpnManager vpnManager = mInjector.getVpnManager();
+            if (vpnManager != null) {
+                vpnManager.setAlwaysOnVpnPackageForUser(userId, null, false, null);
+            }
         }
 
         // Clear app authorizations to establish VPNs. When DISALLOW_CONFIG_VPN is enforced apps
@@ -1789,6 +1792,7 @@
             return mContext.getSystemService(ConnectivityManager.class);
         }
 
+        @Nullable
         VpnManager getVpnManager() {
             return mContext.getSystemService(VpnManager.class);
         }
@@ -3943,10 +3947,16 @@
     /**
      * @param adminReceiver The admin to add
      * @param refreshing true = update an active admin, no error
+     * @param userHandle which user this admin will be set on
+     * @param provisioningContext additional information for debugging
      */
     @Override
     public void setActiveAdmin(
-            ComponentName adminReceiver, boolean refreshing, int userHandle) {
+            ComponentName adminReceiver,
+            boolean refreshing,
+            int userHandle,
+            @Nullable String provisioningContext
+    ) {
         if (!mHasFeature) {
             return;
         }
@@ -3972,6 +3982,7 @@
                 newAdmin.testOnlyAdmin =
                         (existingAdmin != null) ? existingAdmin.testOnlyAdmin
                                 : isPackageTestOnly(adminReceiver.getPackageName(), userHandle);
+                newAdmin.setProvisioningContext(provisioningContext);
                 policy.mAdminMap.put(adminReceiver, newAdmin);
                 int replaceIndex = -1;
                 final int N = policy.mAdminList.size();
@@ -7697,8 +7708,10 @@
                 }
             }
             // If some package is uninstalled after the check above, it will be ignored by CM.
-            if (!mInjector.getVpnManager().setAlwaysOnVpnPackageForUser(
-                    userId, vpnPackage, lockdown, lockdownAllowlist)) {
+            VpnManager vpnManager = mInjector.getVpnManager();
+            if (vpnManager == null
+                    || !mInjector.getVpnManager().setAlwaysOnVpnPackageForUser(
+                            userId, vpnPackage, lockdown, lockdownAllowlist)) {
                 throw new UnsupportedOperationException();
             }
         });
@@ -7746,8 +7759,12 @@
         Preconditions.checkCallAuthorization(
                 isDefaultDeviceOwner(caller) || isProfileOwner(caller));
 
+        VpnManager vpnManager = mInjector.getVpnManager();
+        if (vpnManager == null) {
+            return null;
+        }
         return mInjector.binderWithCleanCallingIdentity(
-                () -> mInjector.getVpnManager().getAlwaysOnVpnPackageForUser(caller.getUserId()));
+                () -> vpnManager.getAlwaysOnVpnPackageForUser(caller.getUserId()));
     }
 
     @Override
@@ -7774,8 +7791,12 @@
                     isDefaultDeviceOwner(caller) || isProfileOwner(caller));
         }
 
+        VpnManager vpnManager = mInjector.getVpnManager();
+        if (vpnManager == null) {
+            return false;
+        }
         return mInjector.binderWithCleanCallingIdentity(
-                () -> mInjector.getVpnManager().isVpnLockdownEnabled(caller.getUserId()));
+                () -> vpnManager.isVpnLockdownEnabled(caller.getUserId()));
     }
 
     @Override
@@ -7797,8 +7818,12 @@
         Preconditions.checkCallAuthorization(
                 isDefaultDeviceOwner(caller) || isProfileOwner(caller));
 
+        VpnManager vpnManager = mInjector.getVpnManager();
+        if (vpnManager == null) {
+            return null;
+        }
         return mInjector.binderWithCleanCallingIdentity(
-                () -> mInjector.getVpnManager().getVpnLockdownAllowlist(caller.getUserId()));
+                () -> vpnManager.getVpnLockdownAllowlist(caller.getUserId()));
     }
 
     private void forceWipeDeviceNoLock(boolean wipeExtRequested, String reason, boolean wipeEuicc,
@@ -12830,7 +12855,7 @@
         });
 
         // Set admin.
-        setActiveAdmin(profileOwner, /* refreshing= */ true, userId);
+        setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null);
         setProfileOwner(profileOwner, userId);
 
         synchronized (getLockObject()) {
@@ -21883,7 +21908,7 @@
             @UserIdInt int userId, @UserIdInt int callingUserId, ComponentName adminComponent) {
         final String adminPackage = adminComponent.getPackageName();
         enablePackage(adminPackage, callingUserId);
-        setActiveAdmin(adminComponent, /* refreshing= */ true, userId);
+        setActiveAdmin(adminComponent, /* refreshing= */ true, userId, null);
     }
 
     private void enablePackage(String packageName, @UserIdInt int userId) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
index eb893fc..0cd5b47 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java
@@ -17,6 +17,7 @@
 
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.content.ComponentName;
 import android.os.ShellCommand;
 import android.os.SystemClock;
@@ -46,11 +47,13 @@
 
     private static final String USER_OPTION = "--user";
     private static final String DO_ONLY_OPTION = "--device-owner-only";
+    private static final String PROVISIONING_CONTEXT_OPTION = "--provisioning-context";
 
     private final DevicePolicyManagerService mService;
     private int mUserId = UserHandle.USER_SYSTEM;
     private ComponentName mComponent;
     private boolean mSetDoOnly;
+    private String mProvisioningContext = null;
 
     DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) {
         mService = Objects.requireNonNull(service);
@@ -127,15 +130,28 @@
         pw.printf("    Lists the device / profile owners per user \n\n");
         pw.printf("  %s\n", CMD_LIST_POLICY_EXEMPT_APPS);
         pw.printf("    Lists the apps that are exempt from policies\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
-                CMD_SET_ACTIVE_ADMIN, USER_OPTION);
-        pw.printf("    Sets the given component as active admin for an existing user.\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
-                + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION);
-        pw.printf("    Sets the given component as active admin, and its package as device owner."
-                + "\n\n");
-        pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
-                CMD_SET_PROFILE_OWNER, USER_OPTION);
+        if (Flags.provisioningContextParameter()) {
+            pw.printf("  %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_ACTIVE_ADMIN, USER_OPTION, PROVISIONING_CONTEXT_OPTION);
+            pw.printf("    Sets the given component as active admin for an existing user.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
+                            + " [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION, PROVISIONING_CONTEXT_OPTION);
+            pw.printf("    Sets the given component as active admin, and its package as device"
+                    + " owner.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n",
+                    CMD_SET_PROFILE_OWNER, USER_OPTION, PROVISIONING_CONTEXT_OPTION);
+        } else {
+            pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
+                    CMD_SET_ACTIVE_ADMIN, USER_OPTION);
+            pw.printf("    Sets the given component as active admin for an existing user.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]"
+                    + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION);
+            pw.printf("    Sets the given component as active admin, and its package as device"
+                    + " owner.\n\n");
+            pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
+                    CMD_SET_PROFILE_OWNER, USER_OPTION);
+        }
         pw.printf("    Sets the given component as active admin and profile owner for an existing "
                 + "user.\n\n");
         pw.printf("  %s [ %s <USER_ID> | current ] <COMPONENT>\n",
@@ -243,7 +259,7 @@
 
     private int runSetActiveAdmin(PrintWriter pw) {
         parseArgs();
-        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId);
+        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext);
 
         pw.printf("Success: Active admin set to component %s\n", mComponent.flattenToShortString());
         return 0;
@@ -253,7 +269,12 @@
         parseArgs();
         boolean isAdminAdded = false;
         try {
-            mService.setActiveAdmin(mComponent, /* refreshing= */ false, mUserId);
+            mService.setActiveAdmin(
+                    mComponent,
+                    /* refreshing= */ false,
+                    mUserId,
+                    mProvisioningContext
+            );
             isAdminAdded = true;
         } catch (IllegalArgumentException e) {
             pw.printf("%s was already an admin for user %d. No need to set it again.\n",
@@ -291,7 +312,7 @@
 
     private int runSetProfileOwner(PrintWriter pw) {
         parseArgs();
-        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId);
+        mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext);
 
         try {
             if (!mService.setProfileOwner(mComponent, mUserId)) {
@@ -363,6 +384,8 @@
                 }
             } else if (DO_ONLY_OPTION.equals(opt)) {
                 mSetDoOnly = true;
+            } else if (PROVISIONING_CONTEXT_OPTION.equals(opt)) {
+                mProvisioningContext = getNextArgRequired();
             } else {
                 throw new IllegalArgumentException("Unknown option: " + opt);
             }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
index cbd2847..6e038f9 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
@@ -17,6 +17,7 @@
 package com.android.server.devicepolicy;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.pm.VerifierDeviceIdentity;
 import android.net.wifi.WifiManager;
 import android.os.Build;
@@ -77,13 +78,14 @@
         mMeid = meid;
         mSerialNumber = Build.getSerial();
         WifiManager wifiManager = context.getSystemService(WifiManager.class);
-        Preconditions.checkState(wifiManager != null, "Unable to access WiFi service");
-        final String[] macAddresses = wifiManager.getFactoryMacAddresses();
-        if (macAddresses == null || macAddresses.length == 0) {
-            mMacAddress = "";
-        } else {
-            mMacAddress = macAddresses[0];
+        String macAddress = "";
+        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            final String[] macAddresses = wifiManager.getFactoryMacAddresses();
+            if (macAddresses != null && macAddresses.length > 0) {
+                macAddress = macAddresses[0];
+            }
         }
+        mMacAddress = macAddress;
     }
 
     private static String getPaddedTruncatedString(String input, int maxLength) {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index db4b171..9e8811f 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -153,6 +153,7 @@
 import com.android.server.contextualsearch.ContextualSearchManagerService;
 import com.android.server.coverage.CoverageService;
 import com.android.server.cpu.CpuMonitorService;
+import com.android.server.crashrecovery.CrashRecoveryModule;
 import com.android.server.credentials.CredentialManagerService;
 import com.android.server.criticalevents.CriticalEventLog;
 import com.android.server.devicepolicy.DevicePolicyManagerService;
@@ -381,8 +382,6 @@
                     + "OnDevicePersonalizationSystemService$Lifecycle";
     private static final String UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS =
             "com.android.server.deviceconfig.DeviceConfigInit$Lifecycle";
-    private static final String CRASHRECOVERY_MODULE_LIFECYCLE_CLASS =
-            "com.android.server.crashrecovery.CrashRecoveryModule$Lifecycle";
 
 
     /*
@@ -2939,7 +2938,7 @@
 
         if (Flags.refactorCrashrecovery()) {
             t.traceBegin("StartCrashRecoveryModule");
-            mSystemServiceManager.startService(CRASHRECOVERY_MODULE_LIFECYCLE_CLASS);
+            mSystemServiceManager.startService(CrashRecoveryModule.Lifecycle.class);
             t.traceEnd();
         } else {
             if (Flags.recoverabilityDetection()) {
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
index 63224bb..c54ff5f 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
@@ -25,7 +25,7 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 
@@ -54,7 +54,8 @@
 
         // Save & load.
         AtomicFile atomicFile = new AtomicFile(
-                new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml"));
+                new File(InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(),
+                        "subtypes.xml"));
         AdditionalSubtypeUtils.saveToFile(AdditionalSubtypeMap.of(allSubtypes),
                 InputMethodMap.of(methodMap), atomicFile);
         AdditionalSubtypeMap loadedSubtypes = AdditionalSubtypeUtils.loadFromFile(atomicFile);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index c2a069d..ec9bfa7 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -59,6 +59,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.internal.compat.IPlatformCompat;
+import com.android.internal.inputmethod.DirectBootAwareness;
 import com.android.internal.inputmethod.IInputMethod;
 import com.android.internal.inputmethod.IInputMethodClient;
 import com.android.internal.inputmethod.IInputMethodSession;
@@ -161,6 +162,7 @@
                         .spyStatic(InputMethodUtils.class)
                         .mockStatic(ServiceManager.class)
                         .spyStatic(AdditionalSubtypeMapRepository.class)
+                        .spyStatic(AdditionalSubtypeUtils.class)
                         .startMocking();
 
         mContext = spy(InstrumentationRegistry.getInstrumentation().getContext());
@@ -235,6 +237,7 @@
 
         // The background writer thread in AdditionalSubtypeMapRepository should be stubbed out.
         doNothing().when(AdditionalSubtypeMapRepository::startWriterThread);
+        doReturn(AdditionalSubtypeMap.EMPTY_MAP).when(() -> AdditionalSubtypeUtils.load(anyInt()));
 
         mServiceThread =
                 new ServiceThread(
@@ -267,6 +270,17 @@
         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
         lifecycle.onStart();
 
+        // Certain tests rely on TEST_IME_ID that is installed with AndroidTest.xml.
+        // TODO(b/352615651): Consider just synthesizing test InputMethodInfo then injecting it.
+        AdditionalSubtypeMapRepository.ensureInitializedAndGet(mCallingUserId);
+        final var settings = InputMethodManagerService.queryInputMethodServicesInternal(mContext,
+                mCallingUserId, AdditionalSubtypeMapRepository.get(mCallingUserId),
+                DirectBootAwareness.AUTO);
+        InputMethodSettingsRepository.put(mCallingUserId, settings);
+
+        // Emulate that the user initialization is done.
+        mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown();
+
         // After this boot phase, services can broadcast Intents.
         lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
 
@@ -277,6 +291,8 @@
 
     @After
     public void tearDown() {
+        InputMethodSettingsRepository.remove(mCallingUserId);
+
         if (mInputMethodManagerService != null) {
             mInputMethodManagerService.mInputMethodDeviceConfigs.destroy();
         }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 2857619..3cf895e 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -40,7 +40,7 @@
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
 import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.internal.inputmethod.StartInputFlags;
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java
index 05c243f..36baacc 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessThrottlerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -301,7 +303,7 @@
                     new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 0.25f);
         List<ThrottlingLevel> levels = new ArrayList<>(List.of(level));
         final ThermalBrightnessThrottlingData data = ThermalBrightnessThrottlingData.create(levels);
-        final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY");
+        final SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY");
         final BrightnessThrottler throttler =
                     createThrottlerSupportedWithTempSensor(data, tempSensor);
         assertTrue(throttler.deviceSupportsThrottling());
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 d268637..2b03dc4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -31,6 +31,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.server.display.ExternalDisplayPolicy.ENABLE_ON_CONNECT;
 import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -2423,7 +2424,7 @@
         String testSensorType = "testType";
         Sensor testSensor = TestUtils.createSensor(testSensorType, testSensorName);
 
-        SensorData sensorData = new SensorData(testSensorType, testSensorName,
+        SensorData sensorData = createSensorData(testSensorType, testSensorName,
                 /* minRefreshRate= */ 10f, /* maxRefreshRate= */ 100f);
 
         when(mMockDisplayDeviceConfig.getProximitySensor()).thenReturn(sensorData);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 624c897..8ed38a6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -21,6 +21,7 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -85,7 +86,6 @@
 import com.android.server.display.color.ColorDisplayService;
 import com.android.server.display.config.HighBrightnessModeData;
 import com.android.server.display.config.HysteresisLevels;
-import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.feature.flags.Flags;
 import com.android.server.display.layout.Layout;
@@ -2137,6 +2137,14 @@
     private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
             DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock,
             boolean isEnabled) {
+
+        setUpDisplay(displayId, uniqueId, logicalDisplayMock, displayDeviceMock,
+                displayDeviceConfigMock, isEnabled, "display_name");
+    }
+
+    private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
+            DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock,
+            boolean isEnabled, String displayName) {
         DisplayInfo info = new DisplayInfo();
         DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
         deviceInfo.uniqueId = uniqueId;
@@ -2148,15 +2156,16 @@
         when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
         when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
         when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
+        when(displayDeviceMock.getNameLocked()).thenReturn(displayName);
         when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
         when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
+                createSensorData(Sensor.STRING_TYPE_PROXIMITY));
         when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
         when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
         when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
-                new SensorData());
+                createSensorData());
         when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_LIGHT, null));
+                createSensorData(Sensor.STRING_TYPE_LIGHT));
         when(displayDeviceConfigMock.getScreenOffBrightnessSensorValueToLux())
                 .thenReturn(new int[0]);
         when(displayDeviceConfigMock.getDefaultDozeBrightness())
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
index ebd6614..29f0722 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.display;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -37,7 +38,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.server.display.config.SensorData;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.Before;
@@ -75,7 +75,7 @@
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
         when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
+                createSensorData(Sensor.STRING_TYPE_PROXIMITY));
         setUpProxSensor();
         DisplayPowerProximityStateController.Injector injector =
                 new DisplayPowerProximityStateController.Injector() {
@@ -165,7 +165,7 @@
 
     @Test
     public void isProximitySensorAvailableReturnsFalseWhenNotAvailableAndNoDefault() {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData());
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
                 mWakelockController, mDisplayDeviceConfig, mTestLooper.getLooper(),
                 mNudgeUpdatePowerState, Display.DEFAULT_DISPLAY,
@@ -176,7 +176,7 @@
     @Test
     public void isProximitySensorAvailableReturnsTrueWhenNotAvailableAndHasDefault()
             throws Exception {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData());
         when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
                 TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -189,7 +189,7 @@
     @Test
     public void isProximitySensorAvailableReturnsFalseWhenNotAvailableHasDefaultNonDefaultDisplay()
             throws Exception {
-        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(new SensorData());
+        when(mDisplayDeviceConfig.getProximitySensor()).thenReturn(createSensorData());
         when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(
                 TestUtils.createSensor(Sensor.TYPE_PROXIMITY, "proximity"));
         mDisplayPowerProximityStateController = new DisplayPowerProximityStateController(
@@ -216,7 +216,7 @@
     public void notifyDisplayDeviceChangedReloadsTheProximitySensor() throws Exception {
         DisplayDeviceConfig updatedDisplayDeviceConfig = mock(DisplayDeviceConfig.class);
         when(updatedDisplayDeviceConfig.getProximitySensor()).thenReturn(
-                new SensorData(Sensor.STRING_TYPE_PROXIMITY, null));
+                createSensorData(Sensor.STRING_TYPE_PROXIMITY));
         Sensor newProxSensor = TestUtils.createSensor(
                 Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_PROXIMITY, 4.0f);
         when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java
index 397d77c..26f6e91 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java
@@ -43,10 +43,14 @@
         mBrightnessEvent = new BrightnessEvent(1);
         mBrightnessEvent.setReason(
                 getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER));
-        mBrightnessEvent.setPhysicalDisplayId("test");
+        mBrightnessEvent.setPhysicalDisplayId("987654321");
+        mBrightnessEvent.setPhysicalDisplayName("display_name");
         mBrightnessEvent.setDisplayState(Display.STATE_ON);
         mBrightnessEvent.setDisplayPolicy(POLICY_BRIGHT);
         mBrightnessEvent.setLux(100.0f);
+        mBrightnessEvent.setPercent(46.5f);
+        mBrightnessEvent.setNits(893.8f);
+        mBrightnessEvent.setUnclampedBrightness(0.65f);
         mBrightnessEvent.setPreThresholdLux(150.0f);
         mBrightnessEvent.setTime(System.currentTimeMillis());
         mBrightnessEvent.setInitialBrightness(25.0f);
@@ -77,12 +81,13 @@
     public void testToStringWorksAsExpected() {
         String actualString = mBrightnessEvent.toString(false);
         String expectedString =
-                "BrightnessEvent: disp=1, physDisp=test, displayState=ON, displayPolicy=BRIGHT,"
-                + " brt=0.6, initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, lux=100.0, preLux=150.0,"
-                + " hbmMax=0.62, hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2,"
-                + " wasShortTermModelActive=true, flags=, reason=doze [ low_pwr ],"
-                + " autoBrightness=true, strategy=" + DISPLAY_BRIGHTNESS_STRATEGY_NAME
-                + ", autoBrightnessMode=idle";
+                "BrightnessEvent: brt=0.6 (46.5%), nits= 893.8, lux=100.0, reason=doze [ "
+                        + "low_pwr ], strat=strategy_name, state=ON, policy=BRIGHT, flags=, "
+                        + "initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, preLux=150.0, "
+                        + "wasShortTermModelActive=true, autoBrightness=true (idle), "
+                        + "unclampedBrt=0.65, hbmMax=0.62, hbmMode=off, thrmMax=0.65, "
+                        + "rbcStrength=-1, powerFactor=0.2, physDisp=display_name(987654321), "
+                        + "logicalId=1";
         assertEquals(expectedString, actualString);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index e982153..0ce9233 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -41,8 +42,8 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.display.DisplayBrightnessState;
-import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.brightness.BrightnessReason;
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
 import com.android.server.display.feature.DisplayManagerFlags;
@@ -89,6 +90,10 @@
     @Mock
     private BrightnessModifier mMockModifier;
     @Mock
+    private TestStatefulModifier mMockStatefulModifier;
+    @Mock
+    private TestDisplayListenerModifier mMockDisplayListenerModifier;
+    @Mock
     private DisplayManagerInternal.DisplayPowerRequest mMockRequest;
 
     @Mock
@@ -99,7 +104,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTestInjector = new TestInjector(List.of(mMockClamper), List.of(mMockModifier));
+        mTestInjector = new TestInjector(List.of(mMockClamper),
+                List.of(mMockModifier, mMockStatefulModifier, mMockDisplayListenerModifier));
         when(mMockDisplayDeviceData.getDisplayId()).thenReturn(DISPLAY_ID);
         when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData);
 
@@ -168,6 +174,13 @@
     }
 
     @Test
+    public void testOnDisplayChanged_DelegatesToDisplayListeners() {
+        mClamperController.onDisplayChanged(mMockDisplayDeviceData);
+
+        verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData);
+    }
+
+    @Test
     public void testOnDisplayChanged_doesNotRestartLightSensor() {
         mClamperController.onDisplayChanged(mMockDisplayDeviceData);
 
@@ -189,6 +202,8 @@
         mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON);
 
         verify(mMockModifier).apply(eq(mMockRequest), any());
+        verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any());
+        verify(mMockStatefulModifier).apply(eq(mMockRequest), any());
     }
 
     @Test
@@ -326,11 +341,41 @@
         verify(mMockClamper).stop();
     }
 
+    @Test
+    public void test_doesNotNotifyExternalListener_aggregatedStateNotChanged() {
+        mTestInjector.mCapturedChangeListener.onChanged();
+        mTestHandler.flush();
+
+        verify(mMockExternalListener, never()).onChanged();
+    }
+
+    @Test
+    public void test_notifiesExternalListener_aggregatedStateChanged() {
+        doAnswer((invocation) -> {
+            ModifiersAggregatedState argument = invocation.getArgument(0);
+            // we need to do changes in AggregatedState to trigger onChange
+            argument.mMaxHdrBrightness = 0.5f;
+            return null;
+        }).when(mMockStatefulModifier).applyStateChange(any());
+        mTestInjector.mCapturedChangeListener.onChanged();
+        mTestHandler.flush();
+
+        verify(mMockExternalListener).onChanged();
+    }
+
     private BrightnessClamperController createBrightnessClamperController() {
         return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener,
                 mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager);
     }
 
+    interface TestDisplayListenerModifier extends BrightnessStateModifier,
+            BrightnessClamperController.DisplayDeviceDataListener {
+    }
+
+    interface TestStatefulModifier extends BrightnessStateModifier,
+            BrightnessClamperController.StatefulModifier {
+    }
+
     private class TestInjector extends BrightnessClamperController.Injector {
 
         private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>>
@@ -366,7 +411,7 @@
         @Override
         List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context,
                 Handler handler, BrightnessClamperController.ClamperChangeListener listener,
-                DisplayDeviceConfig displayDeviceConfig) {
+                BrightnessClamperController.DisplayDeviceData displayDeviceData) {
             return mModifiers;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java
index 34f352e..9d16594 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display.brightness.clamper;
 
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -189,7 +191,7 @@
         final int severity = PowerManager.THERMAL_STATUS_SEVERE;
         IThermalEventListener thermalEventListener = captureSkinThermalEventListener();
         // Update config to listen to display type sensor.
-        final SensorData tempSensor = new SensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY");
+        final SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY");
         final TestThermalData thermalData =
                     new TestThermalData(
                         DISPLAY_ID,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
new file mode 100644
index 0000000..5fd848f
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt
@@ -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 com.android.server.display.brightness.clamper
+
+import android.os.IBinder
+import android.view.Display
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData
+
+fun createDisplayDeviceData(
+    displayDeviceConfig: DisplayDeviceConfig,
+    displayToken: IBinder,
+    uniqueDisplayId: String = "displayId",
+    thermalThrottlingDataId: String = "thermalId",
+    powerThrottlingDataId: String = "powerId",
+    width: Int = 100,
+    height: Int = 100,
+    displayId: Int = Display.DEFAULT_DISPLAY
+): DisplayDeviceData {
+    return DisplayDeviceData(
+        uniqueDisplayId,
+        thermalThrottlingDataId,
+        powerThrottlingDataId,
+        displayDeviceConfig,
+        width,
+        height,
+        displayToken,
+        displayId
+    )
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
new file mode 100644
index 0000000..0ed96ae
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt
@@ -0,0 +1,405 @@
+/*
+ * 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.brightness.clamper
+
+import android.hardware.display.DisplayManagerInternal
+import android.os.IBinder
+import android.os.PowerManager.BRIGHTNESS_MAX
+import android.util.Spline
+import android.view.SurfaceControlHdrLayerInfoListener
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayBrightnessState
+import com.android.server.display.DisplayBrightnessState.BRIGHTNESS_NOT_SET
+import com.android.server.display.DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener
+import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO
+import com.android.server.display.brightness.clamper.HdrBrightnessModifier.Injector
+import com.android.server.display.config.HdrBrightnessData
+import com.android.server.display.config.createHdrBrightnessData
+import com.android.server.testutils.OffsettableClock
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+private const val SEND_TIME_TOLERANCE: Long = 100
+
+@SmallTest
+class HdrBrightnessModifierTest {
+
+    private val stoppedClock = OffsettableClock.Stopped()
+    private val testHandler = TestHandler(null, stoppedClock)
+    private val testInjector = TestInjector()
+    private val mockChangeListener = mock<ClamperChangeListener>()
+    private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>()
+    private val mockDisplayBinder = mock<IBinder>()
+    private val mockDisplayBinderOther = mock<IBinder>()
+    private val mockSpline = mock<Spline>()
+    private val mockRequest = mock<DisplayManagerInternal.DisplayPowerRequest>()
+
+    private lateinit var modifier: HdrBrightnessModifier
+    private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder)
+
+    @Test
+    fun `change listener is not called on init`() {
+        initHdrModifier()
+
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `hdr listener registered on init if hdr data is present`() {
+        initHdrModifier()
+
+        assertThat(testInjector.registeredHdrListener).isNotNull()
+        assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinder)
+    }
+
+    @Test
+    fun `hdr listener not registered on init if hdr data is missing`() {
+        initHdrModifier(null)
+
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNull()
+        assertThat(testInjector.registeredToken).isNull()
+    }
+
+    @Test
+    fun `unsubscribes hdr listener when display changed with no hdr data`() {
+        initHdrModifier()
+
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null)
+        modifier.onDisplayChanged(dummyData)
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNull()
+        assertThat(testInjector.registeredToken).isNull()
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `resubscribes hdr listener when display changed with different token`() {
+        initHdrModifier()
+
+        modifier.onDisplayChanged(
+            createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther))
+        testHandler.flush()
+
+        assertThat(testInjector.registeredHdrListener).isNotNull()
+        assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther)
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `test NO_HDR mode`() {
+        initHdrModifier()
+        // screen size = 10_000
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+
+        // hdr size = 900
+        val desiredMaxHdrRatio = 8f
+        setupHdrLayer(width = 30, height = 30, maxHdrRatio = desiredMaxHdrRatio)
+
+        assertModifierState()
+    }
+
+    @Test
+    fun `test NBM_HDR mode`() {
+        initHdrModifier()
+        // screen size = 10_000
+        val transitionPoint = 0.55f
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            transitionPoint = transitionPoint,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        // hdr size = 5_100
+        val desiredMaxHdrRatio = 8f
+        setupHdrLayer(width = 100, height = 51, maxHdrRatio = desiredMaxHdrRatio)
+
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, desiredMaxHdrRatio, mockSpline)).thenReturn(0.85f)
+
+        assertModifierState(
+            maxBrightness = transitionPoint,
+            hdrRatio = desiredMaxHdrRatio,
+            hdrBrightness = transitionPoint,
+            spline = mockSpline
+        )
+    }
+
+    @Test
+    fun `test HBM_HDR mode`() {
+        initHdrModifier()
+        // screen size = 10_000
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            transitionPoint = 0.55f,
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        // hdr size = 7_100
+        val desiredMaxHdrRatio = 8f
+        setupHdrLayer(width = 100, height = 71, maxHdrRatio = desiredMaxHdrRatio)
+
+        val expectedHdrBrightness = 0.92f
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness)
+
+        assertModifierState(
+            hdrRatio = desiredMaxHdrRatio,
+            hdrBrightness = expectedHdrBrightness,
+            spline = mockSpline
+        )
+    }
+
+    @Test
+    fun `test display change no HDR content`() {
+        initHdrModifier()
+        setupDisplay(width = 100, height = 100)
+        assertModifierState()
+        clearInvocations(mockChangeListener)
+        // display change, new instance of HdrBrightnessData
+        setupDisplay(width = 100, height = 100)
+
+        assertModifierState()
+        verify(mockChangeListener, never()).onChanged()
+    }
+
+    @Test
+    fun `test display change with HDR content`() {
+        initHdrModifier()
+        setupDisplay(width = 100, height = 100)
+        setupHdrLayer(width = 100, height = 100, maxHdrRatio = 5f)
+        assertModifierState(
+            hdrBrightness = 0f,
+            hdrRatio = 5f,
+            spline = mockSpline
+        )
+        clearInvocations(mockChangeListener)
+        // display change, new instance of HdrBrightnessData
+        setupDisplay(width = 100, height = 100)
+
+        assertModifierState(
+            hdrBrightness = 0f,
+            hdrRatio = 5f,
+            spline = mockSpline
+        )
+        // new instance of HdrBrightnessData received, notify listener
+        verify(mockChangeListener).onChanged()
+    }
+
+    @Test
+    fun `test ambient lux decrease above maxBrightnessLimits no HDR`() {
+        initHdrModifier()
+        modifier.setAmbientLux(1000f)
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            maxBrightnessLimits = mapOf(Pair(500f, 0.6f))
+        ))
+
+        modifier.setAmbientLux(500f)
+        // verify debounce is not scheduled
+        assertThat(testHandler.hasMessagesOrCallbacks()).isFalse()
+
+        assertModifierState()
+        verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any())
+    }
+
+    @Test
+    fun `test ambient lux decrease above maxBrightnessLimits with HDR`() {
+        initHdrModifier()
+        modifier.setAmbientLux(1000f)
+        setupDisplay(width = 200, height = 200, hdrBrightnessData = createHdrBrightnessData(
+            maxBrightnessLimits = mapOf(Pair(500f, 0.6f)),
+            sdrToHdrRatioSpline = mockSpline
+        ))
+        setupHdrLayer(width = 200, height = 200, maxHdrRatio = 8f)
+
+        modifier.setAmbientLux(500f)
+
+        // verify debounce is not scheduled
+        assertThat(testHandler.hasMessagesOrCallbacks()).isFalse()
+
+        val hdrBrightnessFromSdr = 0.83f
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, 8f, mockSpline)).thenReturn(hdrBrightnessFromSdr)
+
+        assertModifierState(
+            hdrBrightness = hdrBrightnessFromSdr,
+            spline = mockSpline,
+            hdrRatio = 8f
+        )
+    }
+
+    @Test
+    fun `test ambient lux decrease below maxBrightnessLimits no HDR`() {
+        initHdrModifier()
+        modifier.setAmbientLux(1000f)
+        setupDisplay(width = 100, height = 100, hdrBrightnessData = createHdrBrightnessData(
+            maxBrightnessLimits = mapOf(Pair(500f, 0.6f))
+        ))
+
+        modifier.setAmbientLux(499f)
+        // verify debounce is not scheduled
+        assertThat(testHandler.hasMessagesOrCallbacks()).isFalse()
+
+        assertModifierState()
+        verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any())
+    }
+
+    @Test
+    fun `test ambient lux decrease below maxBrightnessLimits with HDR`() {
+        initHdrModifier()
+        modifier.setAmbientLux(1000f)
+        val maxBrightness = 0.6f
+        val brightnessDecreaseDebounceMillis = 2800L
+        val animationRate = 0.01f
+        setupDisplay(width = 200, height = 200, hdrBrightnessData = createHdrBrightnessData(
+            maxBrightnessLimits = mapOf(Pair(500f, maxBrightness)),
+            brightnessDecreaseDebounceMillis = brightnessDecreaseDebounceMillis,
+            screenBrightnessRampDecrease = animationRate,
+            sdrToHdrRatioSpline = mockSpline,
+        ))
+        setupHdrLayer(width = 200, height = 200, maxHdrRatio = 8f)
+
+        modifier.setAmbientLux(499f)
+
+        val hdrBrightnessFromSdr = 0.83f
+        whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr(
+            0f, 8f, mockSpline)).thenReturn(hdrBrightnessFromSdr)
+        // debounce with brightnessDecreaseDebounceMillis, no changes to the state just yet
+        assertModifierState(
+            hdrBrightness = hdrBrightnessFromSdr,
+            spline = mockSpline,
+            hdrRatio = 8f
+        )
+
+        // verify debounce is scheduled
+        assertThat(testHandler.hasMessagesOrCallbacks()).isTrue()
+        val msgInfo = testHandler.pendingMessages.peek()
+        assertSendTime(brightnessDecreaseDebounceMillis, msgInfo!!.sendTime)
+        clearInvocations(mockChangeListener)
+
+        // triggering debounce, state changes
+        testHandler.flush()
+
+        verify(mockChangeListener).onChanged()
+
+        assertModifierState(
+            hdrBrightness = maxBrightness,
+            spline = mockSpline,
+            hdrRatio = 8f,
+            maxBrightness = maxBrightness,
+            animationRate = animationRate
+        )
+    }
+
+    private fun setupHdrLayer(width: Int = 100, height: Int = 100, maxHdrRatio: Float = 0.8f) {
+        testInjector.registeredHdrListener!!.onHdrInfoChanged(
+            mockDisplayBinder, 1, width, height, 0, maxHdrRatio
+        )
+        testHandler.flush()
+    }
+
+    private fun setupDisplay(
+        width: Int = 100,
+        height: Int = 100,
+        hdrBrightnessData: HdrBrightnessData? = createHdrBrightnessData(
+            minimumHdrPercentOfScreenForNbm = 0.5f,
+            minimumHdrPercentOfScreenForHbm = 0.7f,
+            transitionPoint = 0.68f,
+            sdrToHdrRatioSpline = mockSpline
+        )
+    ) {
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(hdrBrightnessData)
+        modifier.onDisplayChanged(createDisplayDeviceData(
+            mockDisplayDeviceConfig, mockDisplayBinder,
+            width = width,
+            height = height
+        ))
+        testHandler.flush()
+    }
+
+    private fun initHdrModifier(hdrBrightnessData: HdrBrightnessData? = createHdrBrightnessData()) {
+        whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(hdrBrightnessData)
+        modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData)
+        testHandler.flush()
+    }
+
+    // MsgInfo.sendTime is calculated first by adding SystemClock.uptimeMillis()
+    // (in Handler.sendMessageDelayed) and then by subtracting SystemClock.uptimeMillis()
+    // (in TestHandler.sendMessageAtTime, there might be several milliseconds difference between
+    // SystemClock.uptimeMillis() calls, and subtracted value might be greater than added.
+    private fun assertSendTime(expectedTime: Long, sendTime: Long) {
+        assertThat(sendTime).isAtMost(expectedTime)
+        assertThat(sendTime).isGreaterThan(expectedTime - SEND_TIME_TOLERANCE)
+    }
+
+    private fun assertModifierState(
+        maxBrightness: Float = BRIGHTNESS_MAX,
+        hdrRatio: Float = DEFAULT_MAX_HDR_SDR_RATIO,
+        spline: Spline? = null,
+        hdrBrightness: Float = BRIGHTNESS_NOT_SET,
+        animationRate: Float = CUSTOM_ANIMATION_RATE_NOT_SET
+    ) {
+        val modifierState = ModifiersAggregatedState()
+        modifier.applyStateChange(modifierState)
+
+        assertThat(modifierState.mMaxHdrBrightness).isEqualTo(maxBrightness)
+        assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(hdrRatio)
+        assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(spline)
+
+        val stateBuilder = DisplayBrightnessState.builder()
+        modifier.apply(mockRequest, stateBuilder)
+
+        assertThat(stateBuilder.hdrBrightness).isEqualTo(hdrBrightness)
+        assertThat(stateBuilder.customAnimationRate).isEqualTo(animationRate)
+    }
+
+    internal class TestInjector : Injector() {
+        var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null
+        var registeredToken: IBinder? = null
+
+        override fun registerHdrListener(
+            listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+        ) {
+            registeredHdrListener = listener
+            registeredToken = token
+        }
+
+        override fun unregisterHdrListener(
+            listener: SurfaceControlHdrLayerInfoListener, token: IBinder
+        ) {
+            registeredHdrListener = null
+            registeredToken = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
index b742d02..f59e127 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/LightSensorControllerTest.kt
@@ -26,6 +26,7 @@
 import com.android.server.display.brightness.clamper.LightSensorController.Injector
 import com.android.server.display.brightness.clamper.LightSensorController.LightSensorListener
 import com.android.server.display.config.SensorData
+import com.android.server.display.config.createSensorData
 import com.android.server.display.utils.AmbientFilter
 import org.junit.Before
 import org.mockito.kotlin.any
@@ -51,7 +52,7 @@
     private val mockAmbientFilter: AmbientFilter = mock()
 
     private val testInjector = TestInjector()
-    private val dummySensorData = SensorData()
+    private val dummySensorData = createSensorData()
 
     private lateinit var controller: LightSensorController
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt
index 3b3d6f7..c758033 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/config/DisplayDeviceConfigTestUtils.kt
@@ -24,6 +24,17 @@
 import java.io.OutputStreamWriter
 import org.xmlpull.v1.XmlSerializer
 
+@JvmOverloads
+fun createSensorData(
+    type: String? = null,
+    name: String? = null,
+    minRefreshRate: Float = 0f,
+    maxRefreshRate: Float = Float.POSITIVE_INFINITY,
+    supportedModes: List<SupportedModeData> = emptyList()
+): SensorData {
+    return SensorData(type, name, minRefreshRate, maxRefreshRate, supportedModes)
+}
+
 fun createRefreshRateData(
     defaultRefreshRate: Int = 60,
     defaultPeakRefreshRate: Int = 60,
@@ -46,6 +57,7 @@
     screenBrightnessRampIncrease: Float = 0.02f,
     brightnessDecreaseDebounceMillis: Long = 3000,
     screenBrightnessRampDecrease: Float = 0.04f,
+    transitionPoint: Float = 0.65f,
     minimumHdrPercentOfScreenForNbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT,
     minimumHdrPercentOfScreenForHbm: Float = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT,
     allowInLowPowerMode: Boolean = false,
@@ -57,6 +69,7 @@
         screenBrightnessRampIncrease,
         brightnessDecreaseDebounceMillis,
         screenBrightnessRampDecrease,
+        transitionPoint,
         minimumHdrPercentOfScreenForNbm,
         minimumHdrPercentOfScreenForHbm,
         allowInLowPowerMode,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt
index 19c6924..917c681 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/config/HdrBrightnessDataTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server.display.config
 
+import android.os.PowerManager
 import android.util.Spline.createSpline
 import androidx.test.filters.SmallTest
 import com.android.server.display.DisplayBrightnessState
@@ -42,7 +43,7 @@
             )
         }
 
-        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) { 0.6f }
         assertThat(hdrBrightnessData).isNotNull()
 
         assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(3000)
@@ -54,6 +55,7 @@
         assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(500f, 0.6f)
         assertThat(hdrBrightnessData.maxBrightnessLimits).containsEntry(600f, 0.7f)
 
+        assertThat(hdrBrightnessData.hbmTransitionPoint).isEqualTo(PowerManager.BRIGHTNESS_MAX)
         assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(
             HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT
         )
@@ -79,10 +81,13 @@
             )
         }
 
-        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        val transitionPoint = 0.6f
+        val hdrBrightnessData =
+            HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint }
         assertThat(hdrBrightnessData).isNotNull()
 
-        assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f)
+        assertThat(hdrBrightnessData!!.hbmTransitionPoint).isEqualTo(transitionPoint)
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f)
         assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f)
         assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse()
 
@@ -100,7 +105,9 @@
             )
         }
 
-        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        val transitionPoint = 0.6f
+        val hdrBrightnessData =
+            HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint }
         assertThat(hdrBrightnessData).isNotNull()
 
         assertThat(hdrBrightnessData!!.brightnessDecreaseDebounceMillis).isEqualTo(0)
@@ -112,6 +119,7 @@
 
         assertThat(hdrBrightnessData.maxBrightnessLimits).hasSize(0)
 
+        assertThat(hdrBrightnessData.hbmTransitionPoint).isEqualTo(transitionPoint)
         assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.2f)
         assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.2f)
         assertThat(hdrBrightnessData.allowInLowPowerMode).isFalse()
@@ -125,7 +133,7 @@
     fun `test HdrBrightnessData configuration no configuration`() {
         val displayConfiguration = createDisplayConfiguration()
 
-        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration) { 0.6f }
         assertThat(hdrBrightnessData).isNull()
     }
 
@@ -144,10 +152,13 @@
             )
         }
 
-        val hdrBrightnessData = HdrBrightnessData.loadConfig(displayConfiguration)
+        val transitionPoint = 0.6f
+        val hdrBrightnessData =
+            HdrBrightnessData.loadConfig(displayConfiguration) { transitionPoint }
         assertThat(hdrBrightnessData).isNotNull()
 
-        assertThat(hdrBrightnessData!!.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f)
+        assertThat(hdrBrightnessData!!.hbmTransitionPoint).isEqualTo(transitionPoint)
+        assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForNbm).isEqualTo(0.3f)
         assertThat(hdrBrightnessData.minimumHdrPercentOfScreenForHbm).isEqualTo(0.6f)
         assertThat(hdrBrightnessData.allowInLowPowerMode).isTrue()
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
index 6e2d954..c0f5e7a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/utils/SensorUtilsTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display.utils;
 
+import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.when;
@@ -65,7 +67,7 @@
 
     @Test
     public void testNoSensorManager() {
-        Sensor result = SensorUtils.findSensor(null, new SensorData(), Sensor.TYPE_LIGHT);
+        Sensor result = SensorUtils.findSensor(null, createSensorData(), Sensor.TYPE_LIGHT);
         assertNull(result);
     }
 
@@ -123,7 +125,7 @@
         when(mSensorManager.getSensorList(Sensor.TYPE_ALL)).thenReturn(allSensors);
         when(mSensorManager.getDefaultSensor(fallbackType)).thenReturn(defaultSensor);
 
-        SensorData sensorData = new SensorData(sensorType, sensorName);
+        SensorData sensorData = createSensorData(sensorType, sensorName);
 
         Sensor result = SensorUtils.findSensor(mSensorManager, sensorData, fallbackType);
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
index 72883e2..5bd919f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
 
 import android.os.Environment;
 import android.os.FileUtils;
@@ -51,6 +52,7 @@
     private static final int USER_ID_1 = 10;
     private static final int USER_ID_2 = 11;
     private static final int USER_ID_3 = 12;
+    private static final int USER_ID_SYSTEM = 0;
     private static final long TEST_TIMESTAMP = 150_000;
     private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
             .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
@@ -110,6 +112,14 @@
     }
 
     @Test
+    public void testAddWakeupForSystemUser_shouldDoNothing() {
+        mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000);
+        assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+        assertFalse(file.exists());
+    }
+
+    @Test
     public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
         final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
         mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 1dbd532..8656b99 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -500,6 +500,13 @@
         updateOomAdj(app);
         assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
         assertEquals("resumed-split-screen-activity", app.mState.getAdjType());
+
+        doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE
+                | WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM)
+                .when(wpc).getActivityStateFlags();
+        updateOomAdj(app);
+        assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP);
+        assertEquals("perceptible-freeform-activity", app.mState.getAdjType());
     }
 
     @SuppressWarnings("GuardedBy")
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
index 62efbc3..37d8f2f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
@@ -488,6 +488,8 @@
                 new BatteryUsageStats.Builder(new String[]{"CustomConsumer1", "CustomConsumer2"},
                         /* includePowerModels */ true,
                         /* includeProcessStats */ true,
+                        /* includeScreenStateData */ false,
+                        /* includePowerStateData */ false,
                         /* minConsumedPowerThreshold */ 0)
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
@@ -574,7 +576,7 @@
     @Test
     public void testLargeAtomTruncated() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[0], true, false, 0);
+                new BatteryUsageStats.Builder(new String[0], true, false, false, false, 0);
         // If not truncated, this BatteryUsageStats object would generate a proto buffer
         // significantly larger than 50 Kb
         for (int i = 0; i < 3000; i++) {
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 6edfede..624b189 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -397,10 +397,14 @@
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
         final boolean includeProcessStateData = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0;
+        final boolean includeScreenStateData = (query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0;
+        final boolean includePowerStateData = (query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0;
         final double minConsumedPowerThreshold = query.getMinConsumedPowerThreshold();
         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
                 customPowerComponentNames, includePowerModels, includeProcessStateData,
-                minConsumedPowerThreshold);
+                includeScreenStateData, includePowerStateData, minConsumedPowerThreshold);
         SparseArray<? extends BatteryStats.Uid> uidStats = mBatteryStats.getUidStats();
         for (int i = 0; i < uidStats.size(); i++) {
             builder.getOrCreateUidBatteryConsumerBuilder(uidStats.valueAt(i));
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index a3f0770..52bb5e8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -31,6 +31,8 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import static java.util.regex.Pattern.quote;
+
 import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryUsageStats;
@@ -91,7 +93,7 @@
         final Parcel parcel = Parcel.obtain();
         parcel.writeParcelable(outBatteryUsageStats, 0);
 
-        assertThat(parcel.dataSize()).isLessThan(12000);
+        assertThat(parcel.dataSize()).isLessThan(100000);
 
         parcel.setDataPosition(0);
 
@@ -161,15 +163,47 @@
         assertThat(dump).contains("Computed drain: 30000");
         assertThat(dump).contains("actual drain: 1000-2000");
         assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms");
-        assertThat(dump).contains("cpu(fg): 2333 apps: 1333 duration: 3s 332ms");
-        assertThat(dump).contains("cpu(bg): 2444 apps: 1444 duration: 4s 442ms");
-        assertThat(dump).contains("cpu(fgs): 2555 apps: 1555 duration: 5s 552ms");
-        assertThat(dump).contains("cpu(cached): 123 apps: 123 duration: 456ms");
         assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms");
-        assertThat(dump).contains("UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123 "
-                + "( screen=300 cpu=400 (600ms) cpu:fg=1777 (7s 771ms) cpu:bg=1888 (8s 881ms) "
-                + "cpu:fgs=1999 (9s 991ms) cpu:cached=123 (456ms) FOO=500 )");
-        assertThat(dump).contains("User 42: 30.0 ( cpu=10.0 (30ms) FOO=20.0 )");
+        assertThat(dump).containsMatch(quote("(on battery, screen on)") + "\\s*"
+                + "cpu: 2333 apps: 1333 duration: 3s 332ms");
+        assertThat(dump).containsMatch(quote("(not on battery, screen on)") + "\\s*"
+                + "cpu: 2555 apps: 1555 duration: 5s 552ms");
+        assertThat(dump).containsMatch(quote("(on battery, screen off/doze)") + "\\s*"
+                + "cpu: 2444 apps: 1444 duration: 4s 442ms");
+        assertThat(dump).containsMatch(quote("(not on battery, screen off/doze)") + "\\s*"
+                + "cpu: 123 apps: 123 duration: 456ms");
+        assertThat(dump).containsMatch(
+                "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*"
+                        + quote("screen=300 cpu=5787 (27s 99ms) cpu:fg=1777 (7s 771ms) "
+                        + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) "
+                        + "cpu:cached=123 (456ms) FOO=500") + "\\s*"
+                        + quote("(on battery, screen on)") + "\\s*"
+                        + quote("cpu:fg=1777 (7s 771ms)"));
+        assertThat(dump).containsMatch("User 42: 30.0\\s*"
+                + quote("cpu=10.0 (30ms) FOO=20.0"));
+    }
+
+    @Test
+    public void testDumpNoScreenOrPowerState() {
+        final BatteryUsageStats stats = buildBatteryUsageStats1(true, false, false).build();
+        final StringWriter out = new StringWriter();
+        try (PrintWriter pw = new PrintWriter(out)) {
+            stats.dump(pw, "  ");
+        }
+        final String dump = out.toString();
+
+        assertThat(dump).contains("Capacity: 4000");
+        assertThat(dump).contains("Computed drain: 30000");
+        assertThat(dump).contains("actual drain: 1000-2000");
+        assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms");
+        assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms");
+        assertThat(dump).containsMatch(
+                "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*"
+                        + quote("screen=300 cpu=5787 (600ms) cpu:fg=1777 (7s 771ms) "
+                        + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) "
+                        + "cpu:cached=123 (456ms) FOO=500"));
+        assertThat(dump).containsMatch("User 42: 30.0\\s*"
+                + quote("cpu=10.0 (30ms) FOO=20.0"));
     }
 
     @Test
@@ -186,9 +220,8 @@
     public void testAdd() {
         final BatteryUsageStats stats1 = buildBatteryUsageStats1(false).build();
         final BatteryUsageStats stats2 = buildBatteryUsageStats2(new String[]{"FOO"}, true).build();
-
         final BatteryUsageStats sum =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0)
                         .add(stats1)
                         .add(stats2)
                         .build();
@@ -200,14 +233,14 @@
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 2124, null,
-                        5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 745,
+                        5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 11772,
                         POWER_MODEL_UNDEFINED,
                         956, 1167, 1478,
                         true, 3554, 3776, 3998, 444, 3554, 15542, 3776, 17762, 3998, 19982,
                         444, 1110);
             } else if (uidBatteryConsumer.getUid() == APP_UID2) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1332, "bar",
-                        1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444,
+                        1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5985,
                         BatteryConsumer.POWER_MODEL_POWER_PROFILE,
                         555, 666, 777,
                         true, 1777, 1888, 1999, 321, 1777, 7771, 1888, 8881, 1999, 9991,
@@ -229,7 +262,7 @@
     @Test
     public void testAdd_customComponentMismatch() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0);
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0);
         final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"BAR"}, false).build();
 
         assertThrows(IllegalArgumentException.class, () -> builder.add(stats));
@@ -238,7 +271,7 @@
     @Test
     public void testAdd_processStateDataMismatch() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0);
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0);
         final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"FOO"}, false).build();
 
         assertThrows(IllegalArgumentException.class, () -> builder.add(stats));
@@ -259,15 +292,23 @@
         parser.setInput(in, StandardCharsets.UTF_8.name());
         final BatteryUsageStats fromXml = BatteryUsageStats.createFromXml(parser);
 
+        System.out.println("stats = " + stats);
+        System.out.println("fromXml = " + fromXml);
         assertBatteryUsageStats1(fromXml, true);
     }
 
     private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer) {
+        return buildBatteryUsageStats1(includeUserBatteryConsumer, true, true);
+    }
+
+    private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer,
+            boolean includeScreenState, boolean includePowerState) {
         final MockClock clocks = new MockClock();
         final MockBatteryStatsImpl batteryStats = new MockBatteryStatsImpl(clocks);
 
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true,
+                        includeScreenState, includePowerState, 0)
                         .setBatteryCapacity(4000)
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
@@ -312,7 +353,7 @@
 
         final BatteryUsageStats.Builder builder =
                 new BatteryUsageStats.Builder(customPowerComponentNames, true,
-                        includeProcessStateData, 0);
+                        includeProcessStateData, true, true, 0);
         builder.setDischargePercentage(30)
                 .setDischargedPowerRange(1234, 2345)
                 .setStatsStartTimestamp(2000)
@@ -371,9 +412,15 @@
                 .setUsageDurationForCustomComponentMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration);
         if (builder.isProcessStateDataNeeded()) {
-            final BatteryConsumer.Key cpuFgKey = uidBuilder.getKey(
-                    BatteryConsumer.POWER_COMPONENT_CPU,
-                    BatteryConsumer.PROCESS_STATE_FOREGROUND);
+            final BatteryConsumer.Key cpuFgKey = builder.isScreenStateDataNeeded()
+                    ? uidBuilder.getKey(
+                            BatteryConsumer.POWER_COMPONENT_CPU,
+                            BatteryConsumer.PROCESS_STATE_FOREGROUND,
+                            BatteryConsumer.SCREEN_STATE_ON,
+                            BatteryConsumer.POWER_STATE_BATTERY)
+                    : uidBuilder.getKey(
+                            BatteryConsumer.POWER_COMPONENT_CPU,
+                            BatteryConsumer.PROCESS_STATE_FOREGROUND);
             final BatteryConsumer.Key cpuBgKey = uidBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
                     BatteryConsumer.PROCESS_STATE_BACKGROUND);
@@ -401,9 +448,9 @@
 
     private void addAggregateBatteryConsumer(BatteryUsageStats.Builder builder, int scope,
             double consumedPower, int cpuPower, int customComponentPower, int cpuDuration,
-            int customComponentDuration, double cpuPowerForeground, long cpuDurationForeground,
-            double cpuPowerBackground, long cpuDurationBackground, double cpuPowerFgs,
-            long cpuDurationFgs, double cpuPowerCached, long cpuDurationCached) {
+            int customComponentDuration, double cpuPowerBatScrOn, long cpuDurationBatScrOn,
+            double cpuPowerBatScrOff, long cpuDurationBatScrOff, double cpuPowerChgScrOn,
+            long cpuDurationChgScrOn, double cpuPowerChgScrOff, long cpuDurationChgScrOff) {
         final AggregateBatteryConsumer.Builder aggBuilder =
                 builder.getAggregateBatteryConsumerBuilder(scope)
                         .setConsumedPower(consumedPower)
@@ -417,32 +464,40 @@
                         .setUsageDurationForCustomComponentMillis(
                                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
                                 customComponentDuration);
-        if (builder.isProcessStateDataNeeded()) {
-            final BatteryConsumer.Key cpuFgKey = aggBuilder.getKey(
+        if (builder.isPowerStateDataNeeded() || builder.isScreenStateDataNeeded()) {
+            final BatteryConsumer.Key cpuBatScrOn = aggBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
-                    BatteryConsumer.PROCESS_STATE_FOREGROUND);
-            final BatteryConsumer.Key cpuBgKey = aggBuilder.getKey(
+                    BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                    BatteryConsumer.SCREEN_STATE_ON,
+                    BatteryConsumer.POWER_STATE_BATTERY);
+            final BatteryConsumer.Key cpuBatScrOff = aggBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
-                    BatteryConsumer.PROCESS_STATE_BACKGROUND);
-            final BatteryConsumer.Key cpuFgsKey = aggBuilder.getKey(
+                    BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                    BatteryConsumer.SCREEN_STATE_OTHER,
+                    BatteryConsumer.POWER_STATE_BATTERY);
+            final BatteryConsumer.Key cpuChgScrOn = aggBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
-                    BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
-            final BatteryConsumer.Key cpuCachedKey = aggBuilder.getKey(
+                    BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                    BatteryConsumer.SCREEN_STATE_ON,
+                    BatteryConsumer.POWER_STATE_OTHER);
+            final BatteryConsumer.Key cpuChgScrOff = aggBuilder.getKey(
                     BatteryConsumer.POWER_COMPONENT_CPU,
-                    BatteryConsumer.PROCESS_STATE_CACHED);
+                    BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
+                    BatteryConsumer.SCREEN_STATE_OTHER,
+                    BatteryConsumer.POWER_STATE_OTHER);
             aggBuilder
-                    .setConsumedPower(cpuFgKey, cpuPowerForeground,
+                    .setConsumedPower(cpuBatScrOn, cpuPowerBatScrOn,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                    .setUsageDurationMillis(cpuFgKey, cpuDurationForeground)
-                    .setConsumedPower(cpuBgKey, cpuPowerBackground,
+                    .setUsageDurationMillis(cpuBatScrOn, cpuDurationBatScrOn)
+                    .setConsumedPower(cpuBatScrOff, cpuPowerBatScrOff,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                    .setUsageDurationMillis(cpuBgKey, cpuDurationBackground)
-                    .setConsumedPower(cpuFgsKey, cpuPowerFgs,
+                    .setUsageDurationMillis(cpuBatScrOff, cpuDurationBatScrOff)
+                    .setConsumedPower(cpuChgScrOn, cpuPowerChgScrOn,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                    .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs)
-                    .setConsumedPower(cpuCachedKey, cpuPowerCached,
+                    .setUsageDurationMillis(cpuChgScrOn, cpuDurationChgScrOn)
+                    .setConsumedPower(cpuChgScrOff, cpuPowerChgScrOff,
                             BatteryConsumer.POWER_MODEL_POWER_PROFILE)
-                    .setUsageDurationMillis(cpuCachedKey, cpuDurationCached);
+                    .setUsageDurationMillis(cpuChgScrOff, cpuDurationChgScrOff);
         }
     }
 
@@ -456,7 +511,7 @@
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1200, "foo",
-                        1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400,
+                        1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5787,
                         BatteryConsumer.POWER_MODEL_POWER_PROFILE,
                         500, 600, 800,
                         true, 1777, 1888, 1999, 123, 1777, 7771, 1888, 8881, 1999, 9991, 123, 456);
@@ -568,54 +623,53 @@
                     .isEqualTo(totalPowerCached);
         }
 
-        final BatteryConsumer.Key cpuFgKey = uidBatteryConsumer.getKey(
+        final BatteryConsumer.Dimensions cpuFg = new BatteryConsumer.Dimensions(
                 BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND);
         if (processStateDataIncluded) {
-            assertThat(cpuFgKey).isNotNull();
-            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgKey))
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFg))
                     .isEqualTo(cpuPowerForeground);
-            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgKey))
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFg))
                     .isEqualTo(cpuDurationForeground);
         } else {
-            assertThat(cpuFgKey).isNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFg)).isEqualTo(0);
         }
 
-        final BatteryConsumer.Key cpuBgKey = uidBatteryConsumer.getKey(
+        final BatteryConsumer.Dimensions cpuBg = new BatteryConsumer.Dimensions(
                 BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND);
         if (processStateDataIncluded) {
-            assertThat(cpuBgKey).isNotNull();
-            assertThat(uidBatteryConsumer.getConsumedPower(cpuBgKey))
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuBg))
                     .isEqualTo(cpuPowerBackground);
-            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBgKey))
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBg))
                     .isEqualTo(cpuDurationBackground);
         } else {
-            assertThat(cpuBgKey).isNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuBg))
+                    .isEqualTo(0);
         }
 
-        final BatteryConsumer.Key cpuFgsKey = uidBatteryConsumer.getKey(
+        final BatteryConsumer.Dimensions cpuFgs = new BatteryConsumer.Dimensions(
                 BatteryConsumer.POWER_COMPONENT_CPU,
                 BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
         if (processStateDataIncluded) {
-            assertThat(cpuFgsKey).isNotNull();
-            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgsKey))
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs))
                     .isEqualTo(cpuPowerFgs);
-            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgsKey))
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgs))
                     .isEqualTo(cpuDurationFgs);
         } else {
-            assertThat(cpuFgsKey).isNotNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs))
+                    .isEqualTo(0);
         }
 
-        final BatteryConsumer.Key cachedKey = uidBatteryConsumer.getKey(
+        final BatteryConsumer.Dimensions cached = new BatteryConsumer.Dimensions(
                 BatteryConsumer.POWER_COMPONENT_CPU,
                 BatteryConsumer.PROCESS_STATE_CACHED);
         if (processStateDataIncluded) {
-            assertThat(cachedKey).isNotNull();
-            assertThat(uidBatteryConsumer.getConsumedPower(cachedKey))
+            assertThat(uidBatteryConsumer.getConsumedPower(cached))
                     .isEqualTo(cpuPowerCached);
-            assertThat(uidBatteryConsumer.getUsageDurationMillis(cachedKey))
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cached))
                     .isEqualTo(cpuDurationCached);
         } else {
-            assertThat(cpuFgsKey).isNotNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cached))
+                    .isEqualTo(0);
         }
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
index 644ae47..005ceee 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
@@ -130,7 +130,7 @@
         boolean inCpuSection = false;
         for (int i = 0; i < lines.length; i++) {
             if (!inCpuSection) {
-                if (lines[i].startsWith("CpuPowerStatsCollector")) {
+                if (lines[i].startsWith("cpu (1)")) {
                     inCpuSection = true;
                 }
             } else if (lines[i].startsWith(" ")) {
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java
index 7bec13f6..1621d47d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerStatsTest.java
@@ -149,9 +149,9 @@
                 .isEqualTo(20000);
         assertThat(ps2.uidStats.size()).isEqualTo(2);
         assertThat(POWER_STATS_LAYOUT.getUidConsumedEnergy(ps2.uidStats.get(APP_UID1), 0))
-                .isEqualTo(14000);
+                .isEqualTo(4000);
         assertThat(POWER_STATS_LAYOUT.getUidConsumedEnergy(ps2.uidStats.get(APP_UID2), 0))
-                .isEqualTo(21000);
+                .isEqualTo(6000);
     }
 
     @Test
@@ -209,8 +209,8 @@
         assertThat(POWER_STATS_LAYOUT.getDevicePowerEstimate(deviceStats))
                 .isWithin(PRECISION).of(expectedPower * 0.75);
 
-        // UID1: estimated power = 14,000 uC = 0.00388 mAh
-        expectedPower = 0.00388;
+        // UID1: estimated power = 4,000 uC = 0.00111 mAh
+        expectedPower = 0.00111;
         ps2.getUidStats(uidStats, APP_UID1,
                 states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
         assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats))
@@ -221,8 +221,8 @@
         assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats))
                 .isWithin(PRECISION).of(expectedPower * 0.75);
 
-        // UID2: estimated power = 21,000 uC = 0.00583 mAh
-        expectedPower = 0.00583;
+        // UID2: estimated power = 6,000 uC = 0.00166 mAh
+        expectedPower = 0.00167;
         ps2.getUidStats(uidStats, APP_UID2,
                 states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
         assertThat(POWER_STATS_LAYOUT.getUidPowerEstimate(uidStats))
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
index 32bfb2c..7f7967b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
@@ -19,7 +19,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.mock;
 
 import android.os.AggregateBatteryConsumer;
@@ -131,9 +130,20 @@
 
     @Test
     public void breakdownByProcState_fullRange() throws Exception {
+        breakdownByProcState_fullRange(false, false);
+    }
+
+    @Test
+    public void breakdownByProcStateScreenAndPower_fullRange() throws Exception {
+        breakdownByProcState_fullRange(true, true);
+    }
+
+    private void breakdownByProcState_fullRange(boolean includeScreenStateData,
+            boolean includePowerStateData) throws Exception {
         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
                 new String[]{"cu570m"}, /* includePowerModels */ false,
-                /* includeProcessStateData */ true, /* powerThreshold */ 0);
+                /* includeProcessStateData */ true, includeScreenStateData,
+                includePowerStateData, /* powerThreshold */ 0);
         exportAggregatedPowerStats(builder, 1000, 10000);
 
         BatteryUsageStats actual = builder.build();
@@ -177,7 +187,7 @@
     public void breakdownByProcState_subRange() throws Exception {
         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
                 new String[]{"cu570m"}, /* includePowerModels */ false,
-                /* includeProcessStateData */ true, /* powerThreshold */ 0);
+                /* includeProcessStateData */ true, true, true, /* powerThreshold */ 0);
         exportAggregatedPowerStats(builder, 3700, 6700);
 
         BatteryUsageStats actual = builder.build();
@@ -209,7 +219,7 @@
     public void combinedProcessStates() throws Exception {
         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
                 new String[]{"cu570m"}, /* includePowerModels */ false,
-                /* includeProcessStateData */ false, /* powerThreshold */ 0);
+                /* includeProcessStateData */ false, true, true, /* powerThreshold */ 0);
         exportAggregatedPowerStats(builder, 1000, 10000);
 
         BatteryUsageStats actual = builder.build();
@@ -229,13 +239,13 @@
         UidBatteryConsumer uidScope = actual.getUidBatteryConsumers().stream()
                 .filter(us -> us.getUid() == APP_UID1).findFirst().orElse(null);
         // There shouldn't be any per-procstate data
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> uidScope.getConsumedPower(new BatteryConsumer.Dimensions(
+        for (int procState = 0; procState < BatteryConsumer.PROCESS_STATE_COUNT; procState++) {
+            if (procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+                assertThat(uidScope.getConsumedPower(new BatteryConsumer.Dimensions(
                         BatteryConsumer.POWER_COMPONENT_CPU,
-                        BatteryConsumer.PROCESS_STATE_FOREGROUND)));
-
-
+                        BatteryConsumer.PROCESS_STATE_FOREGROUND))).isEqualTo(0);
+            }
+        }
         actual.close();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index 758c84a..ef9580c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -101,7 +101,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 2cb02bd..4645156 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class)) {
+                mock(AudioServerPermissionProvider.class), r -> r.run()) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index 037c3c0..b7100ea 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -87,7 +87,7 @@
                 .thenReturn(AppOpsManager.MODE_ALLOWED);
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null,
-                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider);
+                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run());
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 27b552f..746645a 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -78,7 +78,7 @@
         mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
                 mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
-                mock(AudioServerPermissionProvider.class));
+                mock(AudioServerPermissionProvider.class), r -> r.run());
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 8e34ee1..e45ab31 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -160,7 +160,7 @@
                 @NonNull PermissionEnforcer enforcer,
                 AudioServerPermissionProvider permissionProvider) {
             super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
-                    audioPolicy, looper, appOps, enforcer, permissionProvider);
+                    audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run());
         }
 
         public void setDeviceForStream(int stream, int device) {
diff --git a/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java b/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java
new file mode 100644
index 0000000..f698bea
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/autofill/HelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.autofill;
+
+import static com.android.server.autofill.Helper.SaveInfoStats;
+import static com.android.server.autofill.Helper.getSaveInfoStatsFromFillResponses;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveInfo;
+import android.util.SparseArray;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class HelperTest {
+
+    @Test
+    public void testGetSaveInfoStatsFromFillResponses_nullFillResponses() {
+        SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(null);
+
+        assertThat(saveInfoStats.saveInfoCount).isEqualTo(-1);
+        assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(-1);
+    }
+
+    @Test
+    public void testGetSaveInfoStatsFromFillResponses_emptyFillResponseSparseArray() {
+        SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(new SparseArray<>());
+
+        assertThat(saveInfoStats.saveInfoCount).isEqualTo(0);
+        assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetSaveInfoStatsFromFillResponses_singleResponseWithoutSaveInfo() {
+        FillResponse.Builder fillResponseBuilder = new FillResponse.Builder();
+        // Add client state to satisfy the sanity check in FillResponseBuilder.build()
+        Bundle clientState = new Bundle();
+        fillResponseBuilder.setClientState(clientState);
+        FillResponse testFillResponse = fillResponseBuilder.build();
+
+        SparseArray<FillResponse> testFillResponses = new SparseArray<>();
+        testFillResponses.put(0, testFillResponse);
+
+        SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses);
+
+        assertThat(saveInfoStats.saveInfoCount).isEqualTo(0);
+        assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetSaveInfoStatsFromFillResponses_singleResponseWithSaveInfo() {
+        FillResponse.Builder fillResponseBuilder = new FillResponse.Builder();
+        SaveInfo.Builder saveInfoBuilder = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC);
+        fillResponseBuilder.setSaveInfo(saveInfoBuilder.build());
+        FillResponse testFillResponse = fillResponseBuilder.build();
+
+        SparseArray<FillResponse> testFillResponses = new SparseArray<>();
+        testFillResponses.put(0, testFillResponse);
+
+        SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses);
+
+        assertThat(saveInfoStats.saveInfoCount).isEqualTo(1);
+        assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(1);
+    }
+
+    @Test
+    public void testGetSaveInfoStatsFromFillResponses_multipleResponseWithDifferentTypeSaveInfo() {
+        FillResponse.Builder fillResponseBuilder1 = new FillResponse.Builder();
+        SaveInfo.Builder saveInfoBuilder1 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC);
+        fillResponseBuilder1.setSaveInfo(saveInfoBuilder1.build());
+        FillResponse testFillResponse1 = fillResponseBuilder1.build();
+
+        FillResponse.Builder fillResponseBuilder2 = new FillResponse.Builder();
+        SaveInfo.Builder saveInfoBuilder2 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS);
+        fillResponseBuilder2.setSaveInfo(saveInfoBuilder2.build());
+        FillResponse testFillResponse2 = fillResponseBuilder2.build();
+
+        FillResponse.Builder fillResponseBuilder3 = new FillResponse.Builder();
+        SaveInfo.Builder saveInfoBuilder3 = new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_ADDRESS);
+        fillResponseBuilder3.setSaveInfo(saveInfoBuilder3.build());
+        FillResponse testFillResponse3 = fillResponseBuilder3.build();
+
+        SparseArray<FillResponse> testFillResponses = new SparseArray<>();
+        testFillResponses.put(0, testFillResponse1);
+        testFillResponses.put(1, testFillResponse2);
+        testFillResponses.put(2, testFillResponse3);
+
+        SaveInfoStats saveInfoStats = getSaveInfoStatsFromFillResponses(testFillResponses);
+
+        // Save info count is 3. Since two save info share the same save data type, the distinct
+        // save data type count is 2.
+        assertThat(saveInfoStats.saveInfoCount).isEqualTo(3);
+        assertThat(saveInfoStats.saveDataTypeCount).isEqualTo(2);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
index 8a9538f..ebdde94 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
@@ -66,7 +66,7 @@
                 any(UserHandle.class));
         mDpmTestable = new DevicePolicyManagerServiceTestable(getServices(), mSpiedDpmMockContext);
         setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID);
-        mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE);
+        mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE, null);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 54d1138..cbf7935 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.server.webkit;
 
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -66,10 +65,12 @@
     }
 
     @Override
-    public String getUserChosenWebViewProvider(Context context) { return mUserProvider; }
+    public String getUserChosenWebViewProvider() {
+        return mUserProvider;
+    }
 
     @Override
-    public void updateUserSetting(Context context, String newProviderName) {
+    public void updateUserSetting(String newProviderName) {
         mUserProvider = newProviderName;
     }
 
@@ -77,14 +78,14 @@
     public void killPackageDependents(String packageName) {}
 
     @Override
-    public void enablePackageForAllUsers(Context context, String packageName, boolean enable) {
+    public void enablePackageForAllUsers(String packageName, boolean enable) {
         for(int userId : mUsers) {
             enablePackageForUser(packageName, enable, userId);
         }
     }
 
     @Override
-    public void installExistingPackageForAllUsers(Context context, String packageName) {
+    public void installExistingPackageForAllUsers(String packageName) {
         for (int userId : mUsers) {
             installPackageForUser(packageName, userId);
         }
@@ -131,8 +132,7 @@
     }
 
     @Override
-    public List<UserPackage> getPackageInfoForProviderAllUsers(
-            Context context, WebViewProviderInfo info) {
+    public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo info) {
         Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName);
         List<UserPackage> ret = new ArrayList();
         // Loop over defined users, and find the corresponding package for each user.
@@ -185,12 +185,12 @@
     }
 
     @Override
-    public int getMultiProcessSetting(Context context) {
+    public int getMultiProcessSetting() {
         return mMultiProcessSetting;
     }
 
     @Override
-    public void setMultiProcessSetting(Context context, int value) {
+    public void setMultiProcessSetting(int value) {
         mMultiProcessSetting = value;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index e181a51..06479c8 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -104,10 +104,10 @@
         mTestSystemImpl = Mockito.spy(testing);
         if (updateServiceV2()) {
             mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl);
+                    new WebViewUpdateServiceImpl2(mTestSystemImpl);
         } else {
             mWebViewUpdateServiceImpl =
-                    new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+                    new WebViewUpdateServiceImpl(mTestSystemImpl);
         }
     }
 
@@ -140,7 +140,7 @@
             WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) {
         setupWithPackagesAndRelroCount(webviewPackages, numRelros);
         if (userSetting != null) {
-            mTestSystemImpl.updateUserSetting(null, userSetting);
+            mTestSystemImpl.updateUserSetting(userSetting);
         }
         // Add (enabled and valid) package infos for each provider
         setEnabledAndValidPackageInfos(webviewPackages);
@@ -313,7 +313,7 @@
         };
         setupWithPackagesNonDebuggable(packages);
         // Start with the setting pointing to the invalid package
-        mTestSystemImpl.updateUserSetting(null, invalidPackage);
+        mTestSystemImpl.updateUserSetting(invalidPackage);
         mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */,
                     true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature}
                     , 0 /* updateTime */));
@@ -481,7 +481,7 @@
             new WebViewProviderInfo(secondPackage, "", true, false, null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the second package
-        mTestSystemImpl.updateUserSetting(null, secondPackage);
+        mTestSystemImpl.updateUserSetting(secondPackage);
         // Have all packages be enabled, so that we can change provider however we want to
         setEnabledAndValidPackageInfos(packages);
 
@@ -572,7 +572,7 @@
         // Check that the boot time logic re-enables the fallback package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl).enablePackageForAllUsers(
-                Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+                Mockito.eq(testPackage), Mockito.eq(true));
 
         // Fake the message about the enabling having changed the package state,
         // and check we now use that package.
@@ -657,7 +657,7 @@
                     null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the secondary package
-        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
+        mTestSystemImpl.updateUserSetting(secondaryPackage);
         int secondaryUserId = 10;
         int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID;
         if (multiUser) {
@@ -710,7 +710,7 @@
                     null)};
         setupWithPackages(packages);
         // Start with the setting pointing to the secondary package
-        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
+        mTestSystemImpl.updateUserSetting(secondaryPackage);
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
         int newUser = 100;
         mTestSystemImpl.addUser(newUser);
@@ -832,14 +832,13 @@
                         true /* installed */));
 
         // Set user-chosen package
-        mTestSystemImpl.updateUserSetting(null, chosenPackage);
+        mTestSystemImpl.updateUserSetting(chosenPackage);
 
         runWebViewBootPreparationOnMainSync();
 
         // Verify that we switch the setting to point to the current package
-        Mockito.verify(mTestSystemImpl).updateUserSetting(
-                Mockito.anyObject(), Mockito.eq(nonChosenPackage));
-        assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider(null));
+        Mockito.verify(mTestSystemImpl).updateUserSetting(Mockito.eq(nonChosenPackage));
+        assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider());
 
         checkPreparationPhasesForPackage(nonChosenPackage, 1);
     }
@@ -976,7 +975,7 @@
         setEnabledAndValidPackageInfos(packages);
 
         // Start with the setting pointing to the third package
-        mTestSystemImpl.updateUserSetting(null, thirdPackage);
+        mTestSystemImpl.updateUserSetting(thirdPackage);
 
         runWebViewBootPreparationOnMainSync();
         checkPreparationPhasesForPackage(thirdPackage, 1);
@@ -1167,7 +1166,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 5;
         if (multiUser) {
             mTestSystemImpl.addUser(secondaryUserId);
@@ -1220,7 +1219,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 412;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1277,7 +1276,7 @@
 
         setupWithPackages(webviewPackages);
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
         int secondaryUserId = 4;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1290,7 +1289,7 @@
                 0 /* updateTime */, (testHidden ? true : false) /* hidden */));
 
         // Start with the setting pointing to the uninstalled package
-        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
+        mTestSystemImpl.updateUserSetting(uninstalledPackage);
 
         runWebViewBootPreparationOnMainSync();
 
@@ -1458,7 +1457,7 @@
         runWebViewBootPreparationOnMainSync();
         checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */);
 
-        mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue);
+        mTestSystemImpl.setMultiProcessSetting(settingValue);
 
         assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
     }
@@ -1492,7 +1491,7 @@
                 };
         setupWithPackages(packages);
         // Start with the setting pointing to the invalid package
-        mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName);
+        mTestSystemImpl.updateUserSetting(oldSdkPackage.packageName);
 
         mTestSystemImpl.setPackageInfo(newSdkPackage);
         mTestSystemImpl.setPackageInfo(currentSdkPackage);
@@ -1545,8 +1544,7 @@
         // Check that the boot time logic re-enables the default package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl)
-                .enablePackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+                .enablePackageForAllUsers(Mockito.eq(testPackage), Mockito.eq(true));
     }
 
     @Test
@@ -1570,8 +1568,7 @@
         // Check that the boot time logic tries to install the default package.
         runWebViewBootPreparationOnMainSync();
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     @Test
@@ -1598,8 +1595,7 @@
 
         // Check that we try to re-install the default package.
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     /**
@@ -1632,8 +1628,7 @@
 
         // Check that we try to re-install the default package for all users.
         Mockito.verify(mTestSystemImpl)
-                .installExistingPackageForAllUsers(
-                        Matchers.anyObject(), Mockito.eq(testPackage));
+                .installExistingPackageForAllUsers(Mockito.eq(testPackage));
     }
 
     private void testDefaultPackageChosen(PackageInfo packageInfo) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 4bbbc2b..b07940a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -186,6 +186,9 @@
 import org.mockito.MockitoAnnotations;
 import org.xmlpull.v1.XmlPullParserException;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
@@ -205,9 +208,6 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
 @RunWith(ParameterizedAndroidJunit4.class)
@@ -5022,6 +5022,34 @@
     }
 
     @Test
+    @EnableFlags(FLAG_MODES_API)
+    public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() {
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
+        AutomaticZenRule rule =
+                new AutomaticZenRule.Builder("rule", CONDITION_ID)
+                        .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .build();
+        String ruleId =
+                mZenModeHelper.addAutomaticZenRule(
+                        mPkg, rule, UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+        mZenModeHelper.setAutomaticZenRuleState(
+                ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        AutomaticZenRule updateWithDiff =
+                new AutomaticZenRule.Builder(rule).setTriggerDescription("Whenever").build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_USER, "reason",
+                CUSTOM_PKG_UID);
+
+        assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo(
+                CONDITION_TRUE);
+    }
+
+    @Test
     @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
     public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() {
         assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
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 ef944db..5ae5677b9b5 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -47,6 +47,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Resources;
@@ -103,6 +104,7 @@
 import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.LocalServices;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
+import com.android.server.pm.BackgroundUserSoundNotifier;
 
 import org.junit.After;
 import org.junit.Before;
@@ -809,6 +811,32 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_ADD_UI_FOR_SOUNDS_FROM_BACKGROUND_USERS)
+    public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable {
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+
+        var vib = vibrate(service,
+                VibrationEffect.createWaveform(new long[]{100, 100, 100, 100}, 0), ALARM_ATTRS);
+
+        assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+
+
+        service.mIntentReceiver.onReceive(mContextSpy, new Intent(
+                BackgroundUserSoundNotifier.ACTION_MUTE_SOUND));
+
+        assertTrue(waitUntil(s -> vib.hasEnded(), service, TEST_TIMEOUT_MILLIS));
+
+        var statsInfoCaptor = ArgumentCaptor.forClass(VibrationStats.StatsInfo.class);
+        verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS))
+                .writeVibrationReportedAsync(statsInfoCaptor.capture());
+
+        VibrationStats.StatsInfo touchMetrics = statsInfoCaptor.getAllValues().get(0);
+        assertEquals(Vibration.Status.CANCELLED_BY_FOREGROUND_USER.getProtoEnumValue(),
+                touchMetrics.status);
+    }
+
+    @Test
     public void vibrate_withVibrationAttributes_usesCorrespondingAudioUsageInAppOpsManager() {
         VibratorManagerService service = createSystemReadyService();
 
diff --git a/services/tests/wmtests/OWNERS b/services/tests/wmtests/OWNERS
index 78b867f..51519fd 100644
--- a/services/tests/wmtests/OWNERS
+++ b/services/tests/wmtests/OWNERS
@@ -4,4 +4,7 @@
 # Voice Interaction
 per-file *Assist* = file:/core/java/android/service/voice/OWNERS
 
-natanieljr@google.com
\ No newline at end of file
+# Keyboard shortcuts
+per-file res/xml/bookmarks.xml = file:/services/core/java/com/android/server/input/OWNERS
+
+natanieljr@google.com
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
index 896edff..1c33116 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -24,8 +24,8 @@
 
 import android.view.ViewConfiguration;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index b4505fa..24fc7ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2955,7 +2955,8 @@
 
     @Test
     public void testStartingWindowInTaskFragment() {
-        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true)
+                .setVisible(false).build();
         final WindowState startingWindow = createWindowState(
                 new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1);
         activity1.addWindow(startingWindow);
@@ -3011,6 +3012,28 @@
     }
 
     @Test
+    public void testStartingWindowInTaskFragmentWithVisibleTask() {
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        final Task task = activity1.getTask();
+        final Rect taskBounds = task.getBounds();
+        final Rect tfBounds = new Rect(taskBounds.left, taskBounds.top,
+                taskBounds.left + taskBounds.width() / 2, taskBounds.bottom);
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).setParentTask(task)
+                .setBounds(tfBounds).build();
+
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm).build();
+        final WindowState startingWindow = createWindowState(
+                new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1);
+        taskFragment.addChild(activity2);
+        activity2.addWindow(startingWindow);
+        activity2.mStartingData = mock(StartingData.class);
+        activity2.attachStartingWindow(startingWindow);
+
+        assertNull(activity2.mStartingData.mAssociatedTask);
+        assertNull(task.mSharedStartingData);
+    }
+
+    @Test
     public void testTransitionAnimationBounds() {
         removeGlobalMinSizeRestriction();
         final Task task = new TaskBuilder(mSupervisor)
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
index a3252f8..6ad1044 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java
@@ -53,7 +53,7 @@
 @RunWith(WindowTestRunner.class)
 public class ActivityRefresherTests extends WindowTestsBase {
     private Handler mMockHandler;
-    private LetterboxConfiguration mLetterboxConfiguration;
+    private AppCompatConfiguration mAppCompatConfiguration;
 
     private ActivityRecord mActivity;
     private ActivityRefresher mActivityRefresher;
@@ -69,13 +69,13 @@
 
     @Before
     public void setUp() throws Exception {
-        mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(true);
 
         mMockHandler = mock(Handler.class);
@@ -90,7 +90,7 @@
 
     @Test
     public void testShouldRefreshActivity_refreshDisabled() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(false);
         configureActivityAndDisplay();
         mActivityRefresher.addEvaluator(mEvaluatorTrue);
@@ -146,7 +146,7 @@
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled()
             throws Exception {
         mActivityRefresher.addEvaluator(mEvaluatorTrue);
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
         configureActivityAndDisplay();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
new file mode 100644
index 0000000..ddd6d56
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertEquals;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatAspectRatioOverrides}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatAspectRatioOverridesTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatAspectRatioOverridesTest extends WindowTestsBase {
+
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+    @Test
+    public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+            robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ false);
+
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldApplyUserFullscreenOverride(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserFullscreenOverride_falseFullscreenProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+            robot.checkShouldApplyUserFullscreenOverride(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserFullscreenOverride_falseSettingsProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+            robot.checkShouldApplyUserFullscreenOverride(/* expected */ false);
+        });
+    }
+
+
+    @Test
+    public void testShouldApplyUserFullscreenOverride_returnsTrue() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioFullscreen(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+            robot.checkShouldApplyUserFullscreenOverride(/* expected */ true);
+        });
+    }
+
+    @Test
+    public void testShouldEnableUserAspectRatioSettings_falseProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldEnableUserAspectRatioSettings_trueProperty_returnsTrue() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldEnableUserAspectRatioSettings(/* expected */ true);
+        });
+    }
+
+    @Test
+    public void testShouldEnableUserAspectRatioSettings_ignoreOrientation_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserMinAspectRatioOverride_falseProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldEnableUserAspectRatioSettings(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false);
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldEnableUserAspectRatioSettings(/* enabled */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserMinAspectRatioOverride_disabledIgnoreOrientationRequest() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ false);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ false);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ true);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ true);
+        });
+    }
+
+    @Test
+    public void testShouldApplyUserMinAspectRatioOverride_ignoreOrientation_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.conf().enableUserAppAspectRatioSettings(/* enabled */ false);
+            robot.activity().setIgnoreOrientationRequest(/* enabled */ true);
+            robot.activity().createActivityWithComponent();
+            robot.activity().setGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+            robot.checkShouldApplyUserMinAspectRatioOverride(/* expected */ false);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testShouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() {
+        runTestScenario((robot)-> {
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ true);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testShouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue() {
+        runTestScenario((robot)-> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ true);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testShouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ false);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testShouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ false);
+        });
+    }
+
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ false);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
+    public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse() {
+        runTestScenario((robot)-> {
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatio(/* expected */ false);
+        });
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    void runTestScenario(@NonNull Consumer<AspectRatioOverridesRobotTest> consumer) {
+        spyOn(mWm.mAppCompatConfiguration);
+        final AspectRatioOverridesRobotTest robot =
+                new AspectRatioOverridesRobotTest(mWm, mAtm, mSupervisor);
+        consumer.accept(robot);
+    }
+
+    private static class AspectRatioOverridesRobotTest extends AppCompatRobotBase {
+
+        AspectRatioOverridesRobotTest(@NonNull WindowManagerService wm,
+                @NonNull ActivityTaskManagerService atm,
+                @NonNull ActivityTaskSupervisor supervisor) {
+            super(wm, atm, supervisor);
+        }
+
+        void checkShouldApplyUserFullscreenOverride(boolean expected) {
+            assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides()
+                    .shouldApplyUserFullscreenOverride());
+        }
+
+        void checkShouldEnableUserAspectRatioSettings(boolean expected) {
+            assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides()
+                    .shouldEnableUserAspectRatioSettings());
+        }
+
+        void checkShouldApplyUserMinAspectRatioOverride(boolean expected) {
+            assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides()
+                    .shouldApplyUserMinAspectRatioOverride());
+        }
+
+        void checkShouldOverrideMinAspectRatio(boolean expected) {
+            assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides()
+                    .shouldOverrideMinAspectRatio());
+        }
+
+        @NonNull
+        private AppCompatAspectRatioOverrides getTopActivityAppCompatAspectRatioOverrides() {
+            return activity().top().mAppCompatController.getAppCompatAspectRatioOverrides();
+        }
+    }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index 2d94b34..de99f54 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
 import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
@@ -36,6 +37,7 @@
 
 import androidx.annotation.NonNull;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
 import org.junit.Rule;
@@ -286,11 +288,93 @@
         });
     }
 
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ true);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ true);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue() {
+        runTestScenario((robot) -> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ true);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ true);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ false);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prop().enable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ true);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ true);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false);
+        });
+    }
+
+    @Test
+    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false);
+        });
+    }
+
+    @Test
+    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
+    public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse() {
+        runTestScenario((robot) -> {
+            robot.prop().disable(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE);
+            robot.activity().createActivityWithComponent();
+            robot.activity().activateCameraInPolicy(/* isCameraActive */ true);
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* expected */ false);
+        });
+    }
+
     /**
      * Runs a test scenario providing a Robot.
      */
     void runTestScenario(@NonNull Consumer<CameraOverridesRobotTest> consumer) {
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         final CameraOverridesRobotTest robot = new CameraOverridesRobotTest(mWm, mAtm, mSupervisor);
         consumer.accept(robot);
     }
@@ -323,6 +407,11 @@
                     .shouldApplyFreeformTreatmentForCameraCompat(), expected);
         }
 
+        void checkShouldOverrideMinAspectRatioForCamera(boolean expected) {
+            Assert.assertEquals(getAppCompatCameraOverrides()
+                    .shouldOverrideMinAspectRatioForCamera(), expected);
+        }
+
         void checkIsCameraActive(boolean active) {
             Assert.assertEquals(getAppCompatCameraOverrides().isCameraActive(), active);
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index 974a8e8..0b1bb0f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -112,7 +112,7 @@
      * Runs a test scenario providing a Robot.
      */
     void runTestScenario(@NonNull Consumer<DisplayRotationPolicyRobotTest> consumer) {
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         final DisplayRotationPolicyRobotTest robot =
                 new DisplayRotationPolicyRobotTest(mWm, mAtm, mSupervisor);
         consumer.accept(robot);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java
index d568eec..361177f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatComponentPropRobot.java
@@ -32,27 +32,27 @@
  */
 class AppCompatComponentPropRobot {
     @NonNull
-    private final WindowManagerService mWm;
+    private final PackageManager mPackageManager;
 
     AppCompatComponentPropRobot(@NonNull WindowManagerService wm) {
-        mWm = wm;
+        mPackageManager = wm.mContext.getPackageManager();
+        spyOn(mPackageManager);
     }
 
     void enable(@NonNull String propertyName) {
-        setPropertyValue(propertyName, /* enabled */ true);
+        setPropertyValue(propertyName, "", "", /* enabled */ true);
     }
 
     void disable(@NonNull String propertyName) {
-        setPropertyValue(propertyName, /* enabled */ false);
+        setPropertyValue(propertyName, "", "", /* enabled */ false);
     }
 
-    private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+    private void setPropertyValue(@NonNull String propertyName, @NonNull String packageName,
+            @NonNull String className, boolean enabled) {
         final PackageManager.Property property = new PackageManager.Property(propertyName,
-                /* value */ enabled, /* packageName */ "", /* className */ "");
-        final PackageManager pm = mWm.mContext.getPackageManager();
-        spyOn(pm);
+                /* value */ enabled, packageName, className);
         try {
-            doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+            doReturn(property).when(mPackageManager).getProperty(eq(propertyName), anyString());
         } catch (PackageManager.NameNotFoundException e) {
             fail(e.getLocalizedMessage());
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java
similarity index 89%
rename from services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java
index 3fcec96..c952e2f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java
@@ -18,8 +18,8 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -44,14 +44,14 @@
 import java.util.function.Supplier;
 
 /**
- * Tests for the {@link LetterboxConfigurationPersister} class.
+ * Tests for the {@link AppCompatConfigurationPersister} class.
  *
  * Build/Install/Run:
- *  atest WmTests:LetterboxConfigurationPersisterTest
+ *  atest WmTests:AppCompatConfigurationPersisterTest
  */
 @SmallTest
 @Presubmit
-public class LetterboxConfigurationPersisterTest {
+public class AppCompatConfigurationPersisterTest {
 
     private static final long TIMEOUT = 2000L; // 2 secs
 
@@ -61,7 +61,7 @@
 
     private static final String LETTERBOX_CONFIGURATION_TEST_FILENAME = "letterbox_config_test";
 
-    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private AppCompatConfigurationPersister mAppCompatConfigurationPersister;
     private Context mContext;
     private PersisterQueue mPersisterQueue;
     private QueueState mQueueState;
@@ -74,7 +74,7 @@
         mConfigFolder = mContext.getFilesDir();
         mPersisterQueue = new PersisterQueue();
         mQueueState = new QueueState();
-        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(
+        mAppCompatConfigurationPersister = new AppCompatConfigurationPersister(
                 () -> mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForHorizontalReachability),
                 () -> mContext.getResources().getInteger(
@@ -88,12 +88,12 @@
                 LETTERBOX_CONFIGURATION_TEST_FILENAME);
         mQueueListener = queueEmpty -> mQueueState.onItemAdded();
         mPersisterQueue.addListener(mQueueListener);
-        mLetterboxConfigurationPersister.start();
+        mAppCompatConfigurationPersister.start();
     }
 
     @After
     public void tearDown() throws InterruptedException {
-        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        deleteConfiguration(mAppCompatConfigurationPersister, mPersisterQueue);
         waitForCompletion(mPersisterQueue);
         mPersisterQueue.removeListener(mQueueListener);
         stopPersisterSafe(mPersisterQueue);
@@ -102,7 +102,7 @@
     @Test
     public void test_whenStoreIsCreated_valuesAreDefaults() {
         final int positionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
                         false);
         final int defaultPositionForHorizontalReachability =
                 mContext.getResources().getInteger(
@@ -110,7 +110,7 @@
         Assert.assertEquals(defaultPositionForHorizontalReachability,
                 positionForHorizontalReachability);
         final int positionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
+                mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         final int defaultPositionForVerticalReachability =
                 mContext.getResources().getInteger(
                         R.integer.config_letterboxDefaultPositionForVerticalReachability);
@@ -120,16 +120,16 @@
 
     @Test
     public void test_whenUpdatedWithNewValues_valuesAreWritten() {
-        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
+        mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
+        mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
         waitForCompletion(mPersisterQueue);
         final int newPositionForHorizontalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+                mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
                         false);
         final int newPositionForVerticalReachability =
-                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
+                mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
         Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 newPositionForHorizontalReachability);
         Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -139,7 +139,7 @@
     @Test
     public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
         final PersisterQueue firstPersisterQueue = new PersisterQueue();
-        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+        final AppCompatConfigurationPersister firstPersister = new AppCompatConfigurationPersister(
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 mContext.getFilesDir(), firstPersisterQueue, mQueueState,
@@ -152,7 +152,7 @@
         waitForCompletion(firstPersisterQueue);
         stopPersisterSafe(firstPersisterQueue);
         final PersisterQueue secondPersisterQueue = new PersisterQueue();
-        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+        final AppCompatConfigurationPersister secondPersister = new AppCompatConfigurationPersister(
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 mContext.getFilesDir(), secondPersisterQueue, mQueueState,
@@ -174,7 +174,7 @@
     @Test
     public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
         final PersisterQueue firstPersisterQueue = new PersisterQueue();
-        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+        final AppCompatConfigurationPersister firstPersister = new AppCompatConfigurationPersister(
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 mContext.getFilesDir(), firstPersisterQueue, mQueueState,
@@ -198,7 +198,7 @@
         stopPersisterSafe(firstPersisterQueue);
 
         final PersisterQueue secondPersisterQueue = new PersisterQueue();
-        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+        final AppCompatConfigurationPersister secondPersister = new AppCompatConfigurationPersister(
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
                 mContext.getFilesDir(), secondPersisterQueue, mQueueState,
@@ -245,7 +245,7 @@
         return mQueueState.isEmpty();
     }
 
-    private void deleteConfiguration(LetterboxConfigurationPersister persister,
+    private void deleteConfiguration(AppCompatConfigurationPersister persister,
             PersisterQueue persisterQueue) {
         final AtomicFile fileToDelete = new AtomicFile(
                 new File(mConfigFolder, LETTERBOX_CONFIGURATION_TEST_FILENAME));
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
similarity index 68%
rename from services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index e1da913..0a1b16b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -24,48 +24,46 @@
 import androidx.annotation.NonNull;
 
 /**
- * Robot implementation for {@link LetterboxConfiguration}.
+ * Robot implementation for {@link AppCompatConfiguration}.
  */
-class AppCompatLetterboxConfigurationRobot {
+class AppCompatConfigurationRobot {
 
     @NonNull
-    private final LetterboxConfiguration mLetterboxConfiguration;
+    private final AppCompatConfiguration mAppCompatConfiguration;
 
-    AppCompatLetterboxConfigurationRobot(@NonNull LetterboxConfiguration letterboxConfiguration) {
-        mLetterboxConfiguration = letterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
+    AppCompatConfigurationRobot(@NonNull AppCompatConfiguration appCompatConfiguration) {
+        mAppCompatConfiguration = appCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
     }
 
     void enableTranslucentPolicy(boolean enabled) {
-        when(mLetterboxConfiguration.isTranslucentLetterboxingEnabled()).thenReturn(enabled);
+        when(mAppCompatConfiguration.isTranslucentLetterboxingEnabled()).thenReturn(enabled);
     }
 
     void enablePolicyForIgnoringRequestedOrientation(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration)
+        doReturn(enabled).when(mAppCompatConfiguration)
                 .isPolicyForIgnoringRequestedOrientationEnabled();
     }
 
     void enableCameraCompatTreatment(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+        doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatTreatmentEnabled();
     }
 
     void enableCameraCompatTreatmentAtBuildTime(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration)
+        doReturn(enabled).when(mAppCompatConfiguration)
                 .isCameraCompatTreatmentEnabledAtBuildTime();
     }
 
     void enableUserAppAspectRatioFullscreen(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
+        doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled();
     }
 
     void enableUserAppAspectRatioSettings(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
+        doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled();
     }
 
     void enableCameraCompatSplitScreenAspectRatio(boolean enabled) {
-        doReturn(enabled).when(mLetterboxConfiguration)
+        doReturn(enabled).when(mAppCompatConfiguration)
                 .isCameraCompatSplitScreenAspectRatioEnabled();
     }
-
-
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java
similarity index 74%
rename from services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java
index 79e401c..6efd7de 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java
@@ -19,12 +19,12 @@
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 import static com.android.server.wm.testing.Assert.assertThrows;
 
 import static junit.framework.Assert.assertEquals;
@@ -52,35 +52,35 @@
 import java.util.function.BiConsumer;
 
 /**
- * Tests for the {@link LetterboxConfiguration} class.
+ * Tests for the {@link AppCompatConfiguration} class.
  *
  * Build/Install/Run:
- *  atest WmTests:LetterboxConfigurationTest
+ *  atest WmTests:AppCompatConfigurationTest
  */
 @SmallTest
 @Presubmit
-public class LetterboxConfigurationTest {
+public class AppCompatConfigurationTest {
 
     private Context mContext;
-    private LetterboxConfiguration mLetterboxConfiguration;
-    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private AppCompatConfiguration mAppCompatConfiguration;
+    private AppCompatConfigurationPersister mAppCompatConfigurationPersister;
 
     @Before
     public void setUp() throws Exception {
         mContext = getInstrumentation().getTargetContext();
-        mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class);
-        mLetterboxConfiguration = new LetterboxConfiguration(mContext,
-                mLetterboxConfigurationPersister);
+        mAppCompatConfigurationPersister = mock(AppCompatConfigurationPersister.class);
+        mAppCompatConfiguration = new AppCompatConfiguration(mContext,
+                mAppCompatConfigurationPersister);
     }
 
     @Test
     public void test_whenReadingValues_storeIsInvoked() {
         for (boolean halfFoldPose : Arrays.asList(false, true)) {
-            mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose);
-            verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(
+            mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose);
+            verify(mAppCompatConfigurationPersister).getLetterboxPositionForHorizontalReachability(
                     halfFoldPose);
-            mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose);
-            verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(
+            mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose);
+            verify(mAppCompatConfigurationPersister).getLetterboxPositionForVerticalReachability(
                     halfFoldPose);
         }
     }
@@ -88,13 +88,13 @@
     @Test
     public void test_whenSettingValues_updateConfigurationIsInvoked() {
         for (boolean halfFoldPose : Arrays.asList(false, true)) {
-            mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+            mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
                     halfFoldPose);
-            verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+            verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability(
                     eq(halfFoldPose), anyInt());
-            mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+            mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
                     halfFoldPose);
-            verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+            verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability(
                     eq(halfFoldPose), anyInt());
         }
     }
@@ -107,65 +107,65 @@
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         // Starting from left
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         // Starting from right
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         // Starting from left - book mode
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         // Starting from right - book mode
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
         assertForHorizontalMove(
                 /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
                 /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+                AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
     }
 
     @Test
@@ -176,85 +176,85 @@
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         // Starting from top
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         // Starting from bottom
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ false,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         // Starting from top - tabletop mode
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 1,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         // Starting from bottom - tabletop mode
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop);
         assertForVerticalMove(
                 /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
                 /* expectedTime */ 2,
                 /* halfFoldPose */ true,
-                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+                AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
     }
 
     private void assertForHorizontalMove(int from, int expected, int expectedTime,
-            boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
+            boolean halfFoldPose, BiConsumer<AppCompatConfiguration, Boolean> move) {
         // We are in the current position
-        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose))
+        when(mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose))
                 .thenReturn(from);
-        move.accept(mLetterboxConfiguration, halfFoldPose);
-        verify(mLetterboxConfigurationPersister,
+        move.accept(mAppCompatConfiguration, halfFoldPose);
+        verify(mAppCompatConfigurationPersister,
                 times(expectedTime)).setLetterboxPositionForHorizontalReachability(halfFoldPose,
                 expected);
     }
 
     private void assertForVerticalMove(int from, int expected, int expectedTime,
-            boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
+            boolean halfFoldPose, BiConsumer<AppCompatConfiguration, Boolean> move) {
         // We are in the current position
-        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose))
+        when(mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose))
                 .thenReturn(from);
-        move.accept(mLetterboxConfiguration, halfFoldPose);
-        verify(mLetterboxConfigurationPersister,
+        move.accept(mAppCompatConfiguration, halfFoldPose);
+        verify(mAppCompatConfigurationPersister,
                 times(expectedTime)).setLetterboxPositionForVerticalReachability(halfFoldPose,
                 expected);
     }
@@ -262,20 +262,20 @@
     @Test
     public void test_letterboxPositionWhenReachabilityEnabledIsReset() {
         // Check that horizontal reachability is set with correct arguments
-        mLetterboxConfiguration.resetPersistentLetterboxPositionForHorizontalReachability();
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+        mAppCompatConfiguration.resetPersistentLetterboxPositionForHorizontalReachability();
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability(
                 false /* forBookMode */,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER);
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability(
                 true /* forBookMode */,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
 
         // Check that vertical reachability is set with correct arguments
-        mLetterboxConfiguration.resetPersistentLetterboxPositionForVerticalReachability();
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+        mAppCompatConfiguration.resetPersistentLetterboxPositionForVerticalReachability();
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability(
                 false /* forTabletopMode */,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER);
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability(
                 true /* forTabletopMode */,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
     }
@@ -283,16 +283,16 @@
     @Test
     public void test_letterboxPositionWhenReachabilityEnabledIsSet() {
         // Check that horizontal reachability is set with correct arguments
-        mLetterboxConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
+        mAppCompatConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
                 false /* forBookMode */, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability(
                 false /* forBookMode */,
                 LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
 
         // Check that vertical reachability is set with correct arguments
-        mLetterboxConfiguration.setPersistentLetterboxPositionForVerticalReachability(
+        mAppCompatConfiguration.setPersistentLetterboxPositionForVerticalReachability(
                 false /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
-        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+        verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability(
                 false /* forTabletopMode */,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
     }
@@ -300,60 +300,60 @@
     @Test
     public void test_setLetterboxHorizontalPositionMultiplier_validValues() {
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(-1));
+                () -> mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(-1));
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(2));
+                () -> mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(2));
 
         // Does not throw an exception for values [0,1].
-        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0);
-        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
-        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(1);
+        mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0);
+        mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
+        mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(1);
     }
 
     @Test
     public void test_setLetterboxVerticalPositionMultiplier_validValues() {
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(-1));
+                () -> mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(-1));
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(2));
+                () -> mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(2));
 
         // Does not throw an exception for values [0,1].
-        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0);
-        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
-        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1);
+        mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0);
+        mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(1);
     }
 
     @Test
     public void test_setLetterboxBookModePositionMultiplier_validValues() {
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(-1));
+                () -> mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(-1));
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(2));
+                () -> mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(2));
 
         // Does not throw an exception for values [0,1].
-        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0);
-        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0.5f);
-        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(1);
+        mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(0);
+        mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(0.5f);
+        mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(1);
     }
 
     @Test
     public void test_setLetterboxTabletopModePositionMultiplier_validValues() {
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(-1));
+                () -> mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(-1));
         assertThrows(IllegalArgumentException.class,
-                () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(2));
+                () -> mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(2));
 
         // Does not throw an exception for values [0,1].
-        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0);
-        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f);
-        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(1);
+        mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(0);
+        mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f);
+        mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(1);
     }
 
     @Test
     public void test_evaluateThinLetterboxWhenDensityChanges() {
         final Resources rs = mock(Resources.class);
         final DisplayMetrics dm = mock(DisplayMetrics.class);
-        final LetterboxConfigurationPersister lp = mock(LetterboxConfigurationPersister.class);
+        final AppCompatConfigurationPersister lp = mock(AppCompatConfigurationPersister.class);
         spyOn(mContext);
         when(rs.getDisplayMetrics()).thenReturn(dm);
         when(mContext.getResources()).thenReturn(rs);
@@ -361,7 +361,7 @@
                 .thenReturn(100);
         when(rs.getDimensionPixelSize(R.dimen.config_letterboxThinLetterboxHeightDp))
                 .thenReturn(200);
-        final LetterboxConfiguration configuration = new LetterboxConfiguration(mContext, lp);
+        final AppCompatConfiguration configuration = new AppCompatConfiguration(mContext, lp);
 
         // Verify the values are the expected ones
         dm.density = 100;
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
index 35c2ee0..634453f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
@@ -162,7 +162,7 @@
      * Runs a test scenario providing a Robot.
      */
     void runTestScenario(@NonNull Consumer<OrientationOverridesRobotTest> consumer) {
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         final OrientationOverridesRobotTest robot =
                 new OrientationOverridesRobotTest(mWm, mAtm, mSupervisor);
         consumer.accept(robot);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index aa520e9..ad34a6b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -514,7 +514,7 @@
      */
     void runTestScenario(boolean withActivity,
                          @NonNull Consumer<OrientationPolicyRobotTest> consumer) {
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         final OrientationPolicyRobotTest robot =
                 new OrientationPolicyRobotTest(mWm, mAtm, mSupervisor, withActivity);
         consumer.accept(robot);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
index de16e38..92f246b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
@@ -31,7 +31,7 @@
     @NonNull
     private final AppCompatActivityRobot mActivityRobot;
     @NonNull
-    private final AppCompatLetterboxConfigurationRobot mConfigurationRobot;
+    private final AppCompatConfigurationRobot mConfigurationRobot;
     @NonNull
     private final AppCompatComponentPropRobot mOptPropRobot;
 
@@ -42,7 +42,7 @@
         mActivityRobot = new AppCompatActivityRobot(wm, atm, supervisor,
                 displayWidth, displayHeight);
         mConfigurationRobot =
-                new AppCompatLetterboxConfigurationRobot(wm.mLetterboxConfiguration);
+                new AppCompatConfigurationRobot(wm.mAppCompatConfiguration);
         mOptPropRobot = new AppCompatComponentPropRobot(wm);
     }
 
@@ -53,12 +53,12 @@
     }
 
     @NonNull
-    AppCompatLetterboxConfigurationRobot conf() {
+    AppCompatConfigurationRobot conf() {
         return mConfigurationRobot;
     }
 
     @NonNull
-    void applyOnConf(@NonNull Consumer<AppCompatLetterboxConfigurationRobot> consumer) {
+    void applyOnConf(@NonNull Consumer<AppCompatConfigurationRobot> consumer) {
         consumer.accept(mConfigurationRobot);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index 11e6d90..eaa1641 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -88,7 +88,7 @@
     private static final String CAMERA_ID_2 = "camera-2";
     private CameraManager mMockCameraManager;
     private Handler mMockHandler;
-    private LetterboxConfiguration mLetterboxConfiguration;
+    private AppCompatConfiguration mAppCompatConfiguration;
 
     private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
     private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
@@ -98,13 +98,13 @@
 
     @Before
     public void setUp() throws Exception {
-        mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(true);
 
         mMockCameraManager = mock(CameraManager.class);
@@ -228,7 +228,7 @@
 
     @Test
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
index 1c8dc05..12f5714f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
@@ -60,7 +60,7 @@
     private static final String TEST_PACKAGE_1_LABEL = "testPackage1";
     private CameraManager mMockCameraManager;
     private Handler mMockHandler;
-    private LetterboxConfiguration mLetterboxConfiguration;
+    private AppCompatConfiguration mAppCompatConfiguration;
 
     private CameraStateMonitor mCameraStateMonitor;
     private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
@@ -88,13 +88,13 @@
 
     @Before
     public void setUp() throws Exception {
-        mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(true);
 
         mMockCameraManager = mock(CameraManager.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 23a88a1..b687042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,9 +21,19 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING;
+import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
@@ -59,6 +69,10 @@
 @RunWith(WindowTestRunner.class)
 public class DesktopModeLaunchParamsModifierTests extends
         LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> {
+    private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600);
+    private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560);
+    private static final float LETTERBOX_ASPECT_RATIO = 1.3f;
+
     @Before
     public void setUp() throws Exception {
         mActivity = new ActivityBuilder(mAtm).build();
@@ -158,6 +172,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
     public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -169,6 +184,209 @@
                 (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         final int desiredHeight =
                 (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int)
+                ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO);
+
         assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
         assertEquals(desiredWidth, mResult.mBounds.width());
         assertEquals(desiredHeight, mResult.mBounds.height());
@@ -192,6 +410,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_CenterToDisplay() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -207,6 +426,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -222,6 +442,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -237,6 +458,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -252,6 +474,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -267,6 +490,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -282,6 +506,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightBottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -297,6 +522,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutFractionBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -312,6 +538,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -327,6 +554,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -342,6 +570,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -359,6 +588,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -374,6 +604,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -389,6 +620,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -422,6 +654,38 @@
         assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
     }
 
+    private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) {
+        final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE
+                : RESIZE_MODE_UNRESIZEABLE;
+        final Task task = new TaskBuilder(mSupervisor).setActivityType(
+                ACTIVITY_TYPE_STANDARD).setDisplay(display).build();
+        task.setResizeMode(resizeMode);
+        mActivity = new ActivityBuilder(task.mAtmService)
+                .setTask(task)
+                .setScreenOrientation(orientation)
+                .setOnTop(true).build();
+
+        mActivity.onDisplayChanged(display);
+        mActivity.setOccludesParent(true);
+        mActivity.setVisible(true);
+        mActivity.setVisibleRequested(true);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true);
+
+        return task;
+    }
+
+    private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) {
+        final TestDisplayContent display = new TestDisplayContent
+                .Builder(mAtm, displayBounds.width(), displayBounds.height())
+                .setPosition(DisplayContent.POSITION_TOP).build();
+        display.setBounds(displayBounds);
+        display.getConfiguration().densityDpi = DENSITY_DEFAULT;
+        display.getConfiguration().orientation = ORIENTATION_LANDSCAPE;
+        display.getDefaultTaskDisplayArea().setWindowingMode(orientation);
+
+        return display;
+    }
+
     private void setupDesktopModeLaunchParamsModifier() {
         setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true,
                 /*enforceDeviceRestrictions=*/ true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index af0856f..ab0c8d4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1874,8 +1874,8 @@
     @EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION)
     @Test
     public void testRespectNonTopVisibleFixedOrientation() {
-        spyOn(mWm.mLetterboxConfiguration);
-        doReturn(false).when(mWm.mLetterboxConfiguration).isTranslucentLetterboxingEnabled();
+        spyOn(mWm.mAppCompatConfiguration);
+        doReturn(false).when(mWm.mAppCompatConfiguration).isTranslucentLetterboxingEnabled();
         makeDisplayPortrait(mDisplayContent);
         final ActivityRecord nonTopVisible = new ActivityBuilder(mAtm)
                 .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
@@ -2604,7 +2604,7 @@
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
         assertEquals(fixedOrientationLetterboxRatio,
-                mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(),
+                mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(),
                 0 /* delta */);
     }
 
@@ -2647,7 +2647,7 @@
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
         assertEquals(fixedOrientationLetterboxRatio,
-                mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(),
+                mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(),
                 0 /* delta */);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index d7814ac..e9fcc40 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -90,7 +90,7 @@
     private static final String TEST_PACKAGE_1_LABEL = "testPackage1";
     private CameraManager mMockCameraManager;
     private Handler mMockHandler;
-    private LetterboxConfiguration mLetterboxConfiguration;
+    private AppCompatConfiguration mAppCompatConfiguration;
     private ActivityRefresher mActivityRefresher;
 
     private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
@@ -101,13 +101,13 @@
 
     @Before
     public void setUp() throws Exception {
-        mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
                 .thenReturn(true);
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(true);
 
         mMockCameraManager = mock(CameraManager.class);
@@ -185,7 +185,7 @@
 
     @Test
     public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
 
         mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
@@ -239,7 +239,7 @@
 
     @Test
     public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -253,7 +253,7 @@
 
     @Test
     public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+        when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -480,7 +480,7 @@
     @Test
     public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh()
             throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
+        when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
 
@@ -519,7 +519,7 @@
 
     @Test
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
-        when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+        when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
                 .thenReturn(false);
 
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
index b105703..5e8f347 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
@@ -55,7 +55,7 @@
 
     private DisplayRotationImmersiveAppCompatPolicy mPolicy;
 
-    private LetterboxConfiguration mMockLetterboxConfiguration;
+    private AppCompatConfiguration mMockAppCompatConfiguration;
     private ActivityRecord mMockActivityRecord;
     private Task mMockTask;
     private WindowState mMockWindowState;
@@ -77,15 +77,15 @@
         doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity();
         when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true);
 
-        mMockLetterboxConfiguration = mock(LetterboxConfiguration.class);
-        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
+        mMockAppCompatConfiguration = mock(AppCompatConfiguration.class);
+        when(mMockAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
                 .thenReturn(true);
-        when(mMockLetterboxConfiguration
+        when(mMockAppCompatConfiguration
                 .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime())
                     .thenReturn(true);
 
         mPolicy = DisplayRotationImmersiveAppCompatPolicy.createIfNeeded(
-                mMockLetterboxConfiguration, createDisplayRotationMock(),
+                mMockAppCompatConfiguration, createDisplayRotationMock(),
                 mDisplayContent);
     }
 
@@ -206,7 +206,7 @@
 
     @Test
     public void testRotationChoiceEnforcedOnly_featureFlagDisabled_lockNotEnforced() {
-        when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
+        when(mMockAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled())
                 .thenReturn(false);
 
         assertIsRotationLockEnforcedReturnsFalseForAllRotations();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index c42367e..44c7057b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -19,19 +19,12 @@
 import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
 import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
-import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
-import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
 import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
-import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
-import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_ENABLE_FAKE_FOCUS;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -106,21 +99,19 @@
     private Task mTask;
     private DisplayContent mDisplayContent;
     private LetterboxUiController mController;
-    private LetterboxConfiguration mLetterboxConfiguration;
+    private AppCompatConfiguration mAppCompatConfiguration;
     private final Rect mLetterboxedPortraitTaskBounds = new Rect();
 
     @Before
     public void setUp() throws Exception {
         mActivity = setUpActivityWithComponent();
 
-        mLetterboxConfiguration = mWm.mLetterboxConfiguration;
-        spyOn(mLetterboxConfiguration);
+        mAppCompatConfiguration = mWm.mAppCompatConfiguration;
+        spyOn(mAppCompatConfiguration);
 
         mController = new LetterboxUiController(mWm, mActivity);
     }
 
-
-
     @Test
     public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
         final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
@@ -238,7 +229,7 @@
                     /*centerX=*/ 1, /*centerY=*/ 1)
         );
         insets.setRoundedCorners(roundedCorners);
-        mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1);
+        mAppCompatConfiguration.setLetterboxActivityCornersRadius(-1);
 
         assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
     }
@@ -251,7 +242,7 @@
 
         final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
         mainWindow.mInvGlobalScale = invGlobalScale;
-        mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+        mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
 
         doReturn(true).when(mActivity).isInLetterboxAnimation();
         assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
@@ -272,7 +263,7 @@
         final int configurationRadius = 15;
 
         final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null);
-        mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
+        mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius);
 
         mainWindow.mInvGlobalScale = -1f;
         assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow));
@@ -310,7 +301,7 @@
         doReturn(true).when(mainWindow).isOnScreen();
         doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout();
         doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed();
-        doReturn(true).when(mLetterboxConfiguration).isLetterboxActivityCornersRounded();
+        doReturn(true).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded();
         doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize(
                 R.dimen.taskbar_frame_height);
 
@@ -320,164 +311,6 @@
         return mainWindow;
     }
 
-    // shouldApplyUser...Override
-    @Test
-    public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
-                /* value */ true);
-
-        doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserFullscreenOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserFullscreenOverride_falseFullscreenProperty_returnsFalse()
-            throws Exception {
-        prepareActivityThatShouldApplyUserFullscreenOverride();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
-                /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserFullscreenOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserFullscreenOverride_falseSettingsProperty_returnsFalse()
-            throws Exception {
-        prepareActivityThatShouldApplyUserFullscreenOverride();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserFullscreenOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserFullscreenOverride_returnsTrue() {
-        prepareActivityThatShouldApplyUserFullscreenOverride();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserFullscreenOverride());
-    }
-
-    @Test
-    public void testShouldEnableUserAspectRatioSettings_falseProperty_returnsFalse()
-            throws Exception {
-        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldEnableUserAspectRatioSettings());
-    }
-
-    @Test
-    public void testShouldEnableUserAspectRatioSettings_trueProperty_returnsTrue()
-            throws Exception {
-
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
-
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldEnableUserAspectRatioSettings());
-    }
-
-    @Test
-    public void testShouldEnableUserAspectRatioSettings_noIgnoreOrientation_returnsFalse()
-            throws Exception {
-        prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ false);
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true);
-
-        mController = new LetterboxUiController(mWm, mActivity);
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldEnableUserAspectRatioSettings());
-    }
-
-    @Test
-    public void testShouldApplyUserMinAspectRatioOverride_falseProperty_returnsFalse()
-            throws Exception {
-        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldEnableUserAspectRatioSettings());
-    }
-
-    @Test
-    public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse()
-            throws Exception {
-        doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true);
-
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserMinAspectRatioOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserMinAspectRatioOverride_disabledIgnoreOrientationRequest() {
-        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
-        mDisplayContent.setIgnoreOrientationRequest(false);
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserMinAspectRatioOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserMinAspectRatioOverride_returnsTrue() {
-        prepareActivityThatShouldApplyUserMinAspectRatioOverride();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserMinAspectRatioOverride());
-    }
-
-    @Test
-    public void testShouldApplyUserMinAspectRatioOverride_noIgnoreOrientation_returnsFalse() {
-        prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ false);
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldApplyUserMinAspectRatioOverride());
-    }
-
-    private void prepareActivityForShouldApplyUserMinAspectRatioOverride(
-            boolean orientationRequest) {
-        spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides());
-        doReturn(orientationRequest).when(
-                mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
-        mDisplayContent.setIgnoreOrientationRequest(true);
-        doReturn(USER_MIN_ASPECT_RATIO_3_2)
-                .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides())
-                .getUserMinAspectRatioOverrideCode();
-    }
-
-    private void prepareActivityThatShouldApplyUserMinAspectRatioOverride() {
-        prepareActivityForShouldApplyUserMinAspectRatioOverride(/* orientationRequest */ true);
-    }
-
-    private void prepareActivityThatShouldApplyUserFullscreenOverride() {
-        spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides());
-        doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
-        mDisplayContent.setIgnoreOrientationRequest(true);
-        doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN)
-                .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides())
-                .getUserMinAspectRatioOverrideCode();
-    }
-
     // shouldUseDisplayLandscapeNaturalOrientation
 
     @Test
@@ -531,7 +364,7 @@
     @Test
     @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
     public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
 
         mController = new LetterboxUiController(mWm, mActivity);
 
@@ -541,7 +374,7 @@
     @Test
     @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
     public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
 
         mController = new LetterboxUiController(mWm, mActivity);
 
@@ -552,7 +385,7 @@
     @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
     public void testIsCompatFakeFocusEnabled_propertyDisabledAndOverrideEnabled_fakeFocusDisabled()
             throws Exception {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
         mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -564,7 +397,7 @@
     @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS})
     public void testIsCompatFakeFocusEnabled_propertyEnabled_noOverride_fakeFocusEnabled()
             throws Exception {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
         mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -575,7 +408,7 @@
     @Test
     public void testIsCompatFakeFocusEnabled_propertyDisabled_fakeFocusDisabled()
             throws Exception {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
         mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -586,7 +419,7 @@
     @Test
     public void testIsCompatFakeFocusEnabled_propertyEnabled_fakeFocusEnabled()
             throws Exception {
-        doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled();
+        doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled();
         mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true);
 
         mController = new LetterboxUiController(mWm, mActivity);
@@ -595,156 +428,6 @@
     }
 
     @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_overrideEnabled_returnsTrue() {
-        mActivity = setUpActivityWithComponent();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_propertyTrue_overrideEnabled_returnsTrue()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_propertyTrue_overrideDisabled_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_overrideDisabled_returnsFalse() {
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_propertyFalse_overrideEnabled_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO})
-    public void testshouldOverrideMinAspectRatio_propertyFalse_noOverride_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()
-                .shouldOverrideMinAspectRatio());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_overrideEnabled_returnsTrue() {
-        mActivity = setUpActivityWithComponent();
-        doReturn(true).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsTrue()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-        doReturn(true).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertTrue(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideEnabled_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-        doReturn(false).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_propertyTrue_overrideDisabled_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ true);
-        mActivity = setUpActivityWithComponent();
-        doReturn(true).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_overrideDisabled_returnsFalse() {
-        mActivity = setUpActivityWithComponent();
-        doReturn(true).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @EnableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_propertyFalse_overrideEnabled_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-        mActivity = setUpActivityWithComponent();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
-    @DisableCompatChanges({OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA})
-    public void shouldOverrideMinAspectRatioForCamera_propertyFalse_noOverride_returnsFalse()
-            throws Exception {
-        mockThatProperty(PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE, /* value */ false);
-
-        mActivity = setUpActivityWithComponent();
-
-        doReturn(true).when(mActivity.mAppCompatController
-                .getAppCompatCameraOverrides()).isCameraActive();
-
-        assertFalse(mActivity.mAppCompatController.getAppCompatCameraOverrides()
-                .shouldOverrideMinAspectRatioForCamera());
-    }
-
-    @Test
     @EnableCompatChanges({FORCE_RESIZE_APP})
     public void testshouldOverrideForceResizeApp_overrideEnabled_returnsTrue() {
         mController = new LetterboxUiController(mWm, mActivity);
@@ -862,15 +545,15 @@
 
     @Test
     public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() {
-        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
                 .isCameraCompatTreatmentEnabled();
-        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
                 .isCameraCompatTreatmentEnabledAtBuildTime();
-        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
                 .isCameraCompatSplitScreenAspectRatioEnabled();
-        doReturn(false).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(false).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
-        doReturn(1.5f).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(1.5f).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getFixedOrientationLetterboxAspectRatio();
 
         // Recreate DisplayContent with DisplayRotationCompatPolicy
@@ -894,13 +577,13 @@
     @Test
     public void testIsVerticalThinLetterboxed() {
         // Vertical thin letterbox disabled
-        doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxHeightPx();
         assertFalse(mController.isVerticalThinLetterboxed());
         // Define a Task 100x100
         final Task task = mock(Task.class);
         doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
-        doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxHeightPx();
 
         // Vertical thin letterbox disabled without Task
@@ -925,13 +608,13 @@
     @Test
     public void testIsHorizontalThinLetterboxed() {
         // Horizontal thin letterbox disabled
-        doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxWidthPx();
         assertFalse(mController.isHorizontalThinLetterboxed());
         // Define a Task 100x100
         final Task task = mock(Task.class);
         doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
-        doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+        doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration)
                 .getThinLetterboxWidthPx();
 
         // Vertical thin letterbox disabled without Task
@@ -986,7 +669,7 @@
     @Test
     public void testIsLetterboxEducationEnabled() {
         mController.isLetterboxEducationEnabled();
-        verify(mLetterboxConfiguration).getIsEducationEnabled();
+        verify(mAppCompatConfiguration).getIsEducationEnabled();
     }
 
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index 0bf27d1..f93ffb8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -747,6 +747,8 @@
         }
     }
 
+    @android.platform.test.annotations.RequiresFlagsDisabled(
+            com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY)
     @SetupWindows(addWindows = W_INPUT_METHOD)
     @Test
     public void testLaunchRemoteAnimationWithoutImeBehind() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index afe3604..8981f71 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -66,7 +66,7 @@
 import static com.android.server.wm.ActivityRecord.State.STOPPED;
 import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -228,7 +228,7 @@
             boolean horizontalReachability) {
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        final LetterboxConfiguration config = mWm.mLetterboxConfiguration;
+        final AppCompatConfiguration config = mWm.mAppCompatConfiguration;
         config.setTranslucentLetterboxingOverrideEnabled(true);
         config.setLetterboxVerticalPositionMultiplier(0.5f);
         config.setIsVerticalReachabilityEnabled(true);
@@ -353,8 +353,8 @@
                 .build();
         setUpApp(display);
         display.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
 
         final ActivityRecord activity = getActivityBuilderOnSameTask()
                 .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
@@ -1082,9 +1082,9 @@
                 RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
 
         // Simulate the user selecting the fullscreen user aspect ratio override
-        spyOn(activity.mWmService.mLetterboxConfiguration);
+        spyOn(activity.mWmService.mAppCompatConfiguration);
         spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides());
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
+        doReturn(true).when(activity.mWmService.mAppCompatConfiguration)
                 .isUserAppAspectRatioFullscreenEnabled();
         doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN)
                 .when(activity.mAppCompatController.getAppCompatAspectRatioOverrides())
@@ -1869,7 +1869,7 @@
 
         // Portrait fixed app with min aspect ratio higher that aspect ratio override for fixed
         // orientation letterbox.
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
         mActivity.info.setMinAspectRatio(3);
         prepareUnresizable(mActivity, /* maxAspect= */ 0, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -1901,7 +1901,7 @@
 
         // Portrait fixed app with max aspect ratio lower that aspect ratio override for fixed
         // orientation letterbox.
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(3);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(3);
         prepareUnresizable(mActivity, /* maxAspect= */ 2, SCREEN_ORIENTATION_PORTRAIT);
 
         final Rect displayBounds = new Rect(mActivity.mDisplayContent.getBounds());
@@ -1931,7 +1931,7 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
         final float fixedOrientationLetterboxAspectRatio = 1.1f;
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(
                 fixedOrientationLetterboxAspectRatio);
         prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false);
 
@@ -1979,7 +1979,7 @@
         // Activity should be letterboxed with an aspect ratio of 1.01.
         final Rect afterBounds = mActivity.getBounds();
         final float actualAspectRatio = 1f * afterBounds.height() / afterBounds.width();
-        assertEquals(LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW,
+        assertEquals(AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW,
                 actualAspectRatio, DELTA_ASPECT_RATIO_TOLERANCE);
         assertTrue(mActivity.areBoundsLetterboxed());
     }
@@ -2027,9 +2027,9 @@
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
         final float fixedOrientationLetterboxAspectRatio = 1.1f;
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(
                 fixedOrientationLetterboxAspectRatio);
-        mActivity.mWmService.mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps(
+        mActivity.mWmService.mAppCompatConfiguration.setDefaultMinAspectRatioForUnresizableApps(
                 1.5f);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -2049,7 +2049,7 @@
         // Letterbox logic should use config_letterboxDefaultMinAspectRatioForUnresizableApps over
         // config_fixedOrientationLetterboxAspectRatio.
         assertEquals(displayBounds.height(), activityBounds.height());
-        final float defaultAspectRatio = mActivity.mWmService.mLetterboxConfiguration
+        final float defaultAspectRatio = mActivity.mWmService.mAppCompatConfiguration
                 .getDefaultMinAspectRatioForUnresizableApps();
         assertEquals(displayBounds.height() / defaultAspectRatio, activityBounds.width(), 0.5);
     }
@@ -2136,10 +2136,10 @@
         int screenHeight = 1400;
         setUpDisplaySizeWithApp(screenWidth, screenHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration
+        mActivity.mWmService.mAppCompatConfiguration
                         .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true);
 
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -2172,8 +2172,8 @@
         final int displayHeight = 1400;
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        spyOn(mActivity.mWmService.mLetterboxConfiguration);
-        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+        spyOn(mActivity.mWmService.mAppCompatConfiguration);
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
                 .isUserAppAspectRatioFullscreenEnabled();
 
         // Set user aspect ratio override
@@ -2197,8 +2197,8 @@
         final int displayHeight = 1600;
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        spyOn(mActivity.mWmService.mLetterboxConfiguration);
-        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+        spyOn(mActivity.mWmService.mAppCompatConfiguration);
+        doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration)
                 .isUserAppAspectRatioFullscreenEnabled();
 
         // Set user aspect ratio override
@@ -2394,8 +2394,8 @@
             boolean enabled) {
         final ActivityRecord activity = getActivityBuilderOnSameTask().build();
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        spyOn(activity.mWmService.mLetterboxConfiguration);
-        doReturn(enabled).when(activity.mWmService.mLetterboxConfiguration)
+        spyOn(activity.mWmService.mAppCompatConfiguration);
+        doReturn(enabled).when(activity.mWmService.mAppCompatConfiguration)
                 .isUserAppAspectRatioSettingsEnabled();
         // Set user aspect ratio override
         final IPackageManager pm = mAtm.getPackageManager();
@@ -2428,7 +2428,7 @@
                 .build();
         // Setup Letterbox Configuration
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
+        activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
         // Non-resizable portrait activity
         prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight);
@@ -2449,7 +2449,7 @@
                 .build();
         // Setup Letterbox Configuration
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
+        activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
         // Non-resizable portrait activity
         prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
         float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth);
@@ -2471,7 +2471,7 @@
                 .build();
         // Setup Letterbox Configuration
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
+        activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
         // Non-resizable portrait activity
         prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
         float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight);
@@ -2493,7 +2493,7 @@
                 .build();
         // Setup Letterbox Configuration
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
+        activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f);
         // Non-resizable portrait activity
         prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
         float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth);
@@ -2581,7 +2581,7 @@
     public void testOverrideMinAspectRatioExcludePortraitFullscreen() {
         setUpDisplaySizeWithApp(2600, 1600);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f);
 
         // Create a size compat activity on the same task.
         final ActivityRecord activity = getActivityBuilderOnSameTask().build();
@@ -2611,7 +2611,7 @@
         // In this test, the activity is not in fullscreen, so the override is not applied
         setUpDisplaySizeWithApp(2600, 1600);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f);
 
         // Create a size compat activity on the same task.
         final ActivityRecord activity = getActivityBuilderOnSameTask().build();
@@ -2677,10 +2677,10 @@
         int screenHeight = 1600;
         setUpDisplaySizeWithApp(screenWidth, screenHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration
+        mActivity.mWmService.mAppCompatConfiguration
                         .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true);
 
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -2714,11 +2714,11 @@
         int displayHeight = 1600;
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+        mWm.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
 
         // Enable display aspect ratio to take precedence before
         // fixedOrientationLetterboxAspectRatio
-        mWm.mLetterboxConfiguration
+        mWm.mAppCompatConfiguration
                 .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
 
         // Set up resizable app in portrait
@@ -2750,11 +2750,11 @@
         int displayHeight = 1400;
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
+        mWm.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(2f);
 
         // Enable display aspect ratio to take precedence before
         // fixedOrientationLetterboxAspectRatio
-        mWm.mLetterboxConfiguration
+        mWm.mAppCompatConfiguration
                 .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
 
         // Set up resizable app in landscape
@@ -2787,10 +2787,10 @@
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
         // Enable display aspect ratio to take precedence before
         // fixedOrientationLetterboxAspectRatio
-        mWm.mLetterboxConfiguration
+        mWm.mAppCompatConfiguration
                 .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -2814,10 +2814,10 @@
         setUpDisplaySizeWithApp(displayWidth, displayHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
+        mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f);
         // Enable display aspect ratio to take precedence before
         // fixedOrientationLetterboxAspectRatio
-        mWm.mLetterboxConfiguration
+        mWm.mAppCompatConfiguration
                 .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3134,14 +3134,14 @@
                 RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        spyOn(activity.mWmService.mLetterboxConfiguration);
-        doReturn(true).when(activity.mWmService.mLetterboxConfiguration)
+        spyOn(activity.mWmService.mAppCompatConfiguration);
+        doReturn(true).when(activity.mWmService.mAppCompatConfiguration)
                 .isIgnoreOrientationRequestAllowed();
 
         // Display should not be rotated.
         assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
 
-        doReturn(false).when(activity.mWmService.mLetterboxConfiguration)
+        doReturn(false).when(activity.mWmService.mAppCompatConfiguration)
                 .isIgnoreOrientationRequestAllowed();
 
         // Display should be rotated.
@@ -3427,7 +3427,7 @@
         // Case when the reachability would be enabled otherwise
         setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
         mActivity.getWindowConfiguration().setBounds(null);
 
@@ -3442,7 +3442,7 @@
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(2800, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
         final TestSplitOrganizer organizer =
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
@@ -3465,7 +3465,7 @@
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(1000, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
         final TestSplitOrganizer organizer =
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
@@ -3487,7 +3487,7 @@
     public void testIsVerticalReachabilityEnabled_doesNotMatchParentWidth_false() {
         setUpDisplaySizeWithApp(1000, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable landscape-only activity.
@@ -3509,7 +3509,7 @@
     public void testIsVerticalReachabilityEnabled_emptyBounds_true() {
         setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -3526,7 +3526,7 @@
     public void testIsHorizontalReachabilityEnabled_emptyBounds_true() {
         setUpDisplaySizeWithApp(/* dw */ 2800, /* dh */ 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3543,7 +3543,7 @@
     public void testIsHorizontalReachabilityEnabled_portraitDisplayAndApp_true() {
         // Portrait display
         setUpDisplaySizeWithApp(1400, 1600);
-        mActivity.mWmService.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // 16:9f unresizable portrait app
@@ -3557,7 +3557,7 @@
     public void testIsVerticalReachabilityEnabled_landscapeDisplayAndApp_true() {
         // Landscape display
         setUpDisplaySizeWithApp(1600, 1500);
-        mActivity.mWmService.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // 16:9f unresizable landscape app
@@ -3571,7 +3571,7 @@
     public void testIsHorizontalReachabilityEnabled_doesNotMatchParentHeight_false() {
         setUpDisplaySizeWithApp(2800, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable portrait-only activity.
@@ -3593,7 +3593,7 @@
     public void testIsHorizontalReachabilityEnabled_inSizeCompatMode_matchesParentHeight_true() {
         setUpDisplaySizeWithApp(1800, 2200);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable portrait-only activity.
@@ -3615,7 +3615,7 @@
     public void testIsVerticalReachabilityEnabled_inSizeCompatMode_matchesParentWidth_true() {
         setUpDisplaySizeWithApp(2200, 1800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true);
         setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable landscape-only activity.
@@ -3698,7 +3698,7 @@
     @Test
     public void testLetterboxDetailsForStatusBar_letterboxNotOverlappingStatusBar() {
         // Align to center so that we don't overlap with the status bar
-        mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
         final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)
                 .setNotch(100)
                 .build();
@@ -3751,7 +3751,7 @@
         navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
         navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
 
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxActivityCornersRadius(15);
 
         final WindowState w1 = addWindowToActivity(mActivity);
         w1.mAboveInsetsState.addSource(navSource);
@@ -3793,7 +3793,7 @@
         setUpDisplaySizeWithApp(screenWidth, screenHeight);
 
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true);
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1.0f);
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(1.0f);
 
         final InsetsSource navSource = new InsetsSource(
                 InsetsSource.createId(null, 0, navigationBars()), navigationBars());
@@ -3801,7 +3801,7 @@
         // Immersive activity has transient navbar
         navSource.setVisible(!immersive);
         navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxActivityCornersRadius(15);
 
         final WindowState w1 = addWindowToActivity(mActivity);
         w1.mAboveInsetsState.addSource(navSource);
@@ -3909,7 +3909,7 @@
         spyOn(policy);
         doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
                 display.mBaseDisplayHeight, display.mBaseDisplayWidth);
-        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
 
         setUpApp(display);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3974,7 +3974,7 @@
         setUpDisplaySizeWithApp(2800, 1400);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(
                 letterboxHorizontalPositionMultiplier);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
         assertEquals(fixedOrientationLetterbox, mActivity.getBounds());
@@ -4171,7 +4171,7 @@
     public void testApplyAspectRatio_containingRatioAlmostEqualToMaxRatio_boundsUnchanged() {
         setUpDisplaySizeWithApp(1981, 2576);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
 
         final Rect originalBounds = new Rect(mActivity.getBounds());
         prepareUnresizable(mActivity, 1.3f, SCREEN_ORIENTATION_UNSPECIFIED);
@@ -4206,7 +4206,7 @@
         spyOn(policy);
         doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90,
                 display.mBaseDisplayHeight, display.mBaseDisplayWidth);
-        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
 
         setUpApp(display);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -4269,7 +4269,7 @@
         // Set up a display in portrait with a fixed-orientation LANDSCAPE app
         setUpDisplaySizeWithApp(1400, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(
                 1.0f /*letterboxVerticalPositionMultiplier*/);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -4294,7 +4294,7 @@
         // Set up a display in portrait with a fixed-orientation LANDSCAPE app.
         setUpDisplaySizeWithApp(1000, 2000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(
                 1.0f /*letterboxVerticalPositionMultiplier*/);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -4322,9 +4322,9 @@
     public void testGetFixedOrientationLetterboxAspectRatio_tabletop_centered() {
         // Set up a display in portrait with a fixed-orientation LANDSCAPE app
         setUpDisplaySizeWithApp(1400, 2800);
-        mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+        mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(
                 LETTERBOX_POSITION_MULTIPLIER_CENTER);
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(
                 1.0f /*letterboxVerticalPositionMultiplier*/);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -4360,8 +4360,8 @@
         // Set up a display in landscape with a fixed-orientation PORTRAIT app
         setUpDisplaySizeWithApp(2800, 1400);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(true);
-        mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+        mWm.mAppCompatConfiguration.setIsAutomaticReachabilityInBookModeEnabled(true);
+        mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(
                 1.0f /*letterboxHorizontalPositionMultiplier*/);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -4386,8 +4386,8 @@
         // Set up a display in landscape with a fixed-orientation PORTRAIT app
         setUpDisplaySizeWithApp(2800, 1400);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(false);
-        mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
+        mWm.mAppCompatConfiguration.setIsAutomaticReachabilityInBookModeEnabled(false);
+        mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
         prepareUnresizable(mActivity, 1.75f, SCREEN_ORIENTATION_PORTRAIT);
 
         Rect letterboxNoFold = new Rect(1000, 0, 1800, 1400);
@@ -4469,7 +4469,7 @@
         setUpDisplaySizeWithApp(1400, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(
                 letterboxVerticalPositionMultiplier);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -4585,7 +4585,7 @@
     public void testIsEligibleForLetterboxEducation_educationNotEnabled_returnsFalse() {
         setUpDisplaySizeWithApp(2500, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(false);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(false);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -4596,7 +4596,7 @@
     public void testIsEligibleForLetterboxEducation_notEligibleForFixedOrientation_returnsFalse() {
         setUpDisplaySizeWithApp(1000, 2500);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -4609,7 +4609,7 @@
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(1000, 1200);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
         final TestSplitOrganizer organizer =
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
 
@@ -4630,7 +4630,7 @@
     public void testIsEligibleForLetterboxEducation_fixedOrientationLandscape_returnsFalse() {
         setUpDisplaySizeWithApp(1000, 2500);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -4643,7 +4643,7 @@
     public void testIsEligibleForLetterboxEducation_hasStartingWindow_returnsFalseUntilRemoved() {
         setUpDisplaySizeWithApp(2500, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
         mActivity.mStartingData = mock(StartingData.class);
@@ -4666,7 +4666,7 @@
     public void testIsEligibleForLetterboxEducation_hasStartingWindowAndEducationNotEnabled() {
         setUpDisplaySizeWithApp(2500, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(false);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(false);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
         mActivity.mStartingData = mock(StartingData.class);
@@ -4689,7 +4689,7 @@
     public void testIsEligibleForLetterboxEducation_letterboxedForFixedOrientation_returnsTrue() {
         setUpDisplaySizeWithApp(2500, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -4702,7 +4702,7 @@
     public void testIsEligibleForLetterboxEducation_sizeCompatAndEligibleForFixedOrientation() {
         setUpDisplaySizeWithApp(1000, 2500);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true);
+        mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -4879,7 +4879,7 @@
         setUpDisplaySizeWithApp(2800, 1400);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(
                 letterboxHorizontalPositionMultiplier);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
         assertFitted();
@@ -4896,7 +4896,7 @@
         setUpDisplaySizeWithApp(1400, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
 
-        mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+        mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(
                 letterboxVerticalPositionMultiplier);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java
index e3f8e8c..8abf3f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceControlTests.java
@@ -36,8 +36,8 @@
 import android.view.SurfaceView;
 
 import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.wm.utils.CommonUtils;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index a816aa9..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -203,6 +203,7 @@
                 .mockStatic(LockGuard.class, mockStubOnly)
                 .mockStatic(Watchdog.class, mockStubOnly)
                 .spyStatic(DesktopModeHelper.class)
+                .spyStatic(DesktopModeBoundsCalculator.class)
                 .strictness(Strictness.LENIENT)
                 .startMocking();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 76690ec..0bf850a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -611,7 +611,7 @@
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
         final Task task = rootTask.getBottomMostTask();
         final ActivityRecord root = task.getTopNonFinishingActivity();
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         spyOn(root);
         spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides());
 
@@ -655,7 +655,7 @@
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
         final Task task = rootTask.getBottomMostTask();
         final ActivityRecord root = task.getTopNonFinishingActivity();
-        spyOn(mWm.mLetterboxConfiguration);
+        spyOn(mWm.mAppCompatConfiguration);
         spyOn(root);
 
         doReturn(false).when(root).fillsParent();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index f07b402..cbf17c4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -258,7 +258,7 @@
             robot.transparentActivity((ta) -> {
                 ta.applyOnActivity((a) -> {
                     a.applyToTopActivity((topActivity) -> {
-                        topActivity.mWmService.mLetterboxConfiguration
+                        topActivity.mWmService.mAppCompatConfiguration
                                 .setLetterboxHorizontalPositionMultiplier(1.0f);
                     });
                     a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
index 593e983..22def51 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -23,6 +23,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
 
 import android.platform.test.annotations.Presubmit;
@@ -60,4 +61,24 @@
         verify(c).accept(eq(mDockedDividerWindow));
         verify(c).accept(eq(mImeWindow));
     }
+
+    @android.platform.test.annotations.RequiresFlagsEnabled(
+            com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY)
+    @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
+    @Test
+    public void testTraverseImeRegardlessOfImeTarget() {
+        mDisplayContent.setImeLayeringTarget(mAppWindow);
+        mDisplayContent.setImeInputTarget(mAppWindow);
+        mAppWindow.mHasSurface = false;
+        mAppWindow.mActivityRecord.setVisibleRequested(false);
+        mAppWindow.mActivityRecord.setVisible(false);
+
+        final boolean[] foundIme = { false };
+        mDisplayContent.forAllWindows(w -> {
+            if (w == mImeWindow) {
+                foundIme[0] = true;
+            }
+        }, true /* traverseTopToBottom */);
+        assertTrue("IME must be found", foundIme[0]);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 89abe2f..39640fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -47,10 +47,10 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -1390,8 +1390,8 @@
     }
 
     private boolean setupLetterboxConfigurationWithBackgroundType(
-            @LetterboxConfiguration.LetterboxBackgroundType int letterboxBackgroundType) {
-        mWm.mLetterboxConfiguration.setLetterboxBackgroundTypeOverride(letterboxBackgroundType);
+            @AppCompatConfiguration.LetterboxBackgroundType int letterboxBackgroundType) {
+        mWm.mAppCompatConfiguration.setLetterboxBackgroundTypeOverride(letterboxBackgroundType);
         return mWm.isLetterboxBackgroundMultiColored();
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index b512aa8..ea2abf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -141,8 +141,8 @@
 import java.util.List;
 
 /** Common base class for window manager unit test classes. */
-class WindowTestsBase extends SystemServiceTestsBase {
-    final Context mContext = getInstrumentation().getTargetContext();
+public class WindowTestsBase extends SystemServiceTestsBase {
+    protected final Context mContext = getInstrumentation().getTargetContext();
 
     // Default package name
     static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo";
@@ -262,34 +262,34 @@
         // Ensure letterbox aspect ratio is not overridden on any device target.
         // {@link com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio}, is set
         // on some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(0);
+        mAtm.mWindowManager.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(0);
         // Ensure letterbox horizontal position multiplier is not overridden on any device target.
         // {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier},
         // may be set on some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
+        mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
         // Ensure letterbox vertical position multiplier is not overridden on any device target.
         // {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier},
         // may be set on some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.0f);
+        mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.0f);
         // Ensure letterbox horizontal reachability treatment isn't overridden on any device target.
         // {@link com.android.internal.R.bool.config_letterboxIsHorizontalReachabilityEnabled},
         // may be set on some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(false);
+        mAtm.mWindowManager.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(false);
         // Ensure letterbox vertical reachability treatment isn't overridden on any device target.
         // {@link com.android.internal.R.bool.config_letterboxIsVerticalReachabilityEnabled},
         // may be set on some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(false);
+        mAtm.mWindowManager.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(false);
         // Ensure aspect ratio for unresizable apps isn't overridden on any device target.
         // {@link com.android.internal.R.bool
         // .config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled}, may be set on some
         // device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration
+        mAtm.mWindowManager.mAppCompatConfiguration
                 .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(false);
         // Ensure aspect ratio for al apps isn't overridden on any device target.
         // {@link com.android.internal.R.bool
         // .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}, may be set on
         // some device form factors.
-        mAtm.mWindowManager.mLetterboxConfiguration
+        mAtm.mWindowManager.mAppCompatConfiguration
                 .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
 
         // Setup WallpaperController crop utils with a simple center-align strategy
@@ -331,7 +331,7 @@
     private void checkDeviceSpecificOverridesNotApplied() {
         // Check global overrides
         if (!sGlobalOverridesChecked) {
-            assertEquals(0, mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(),
+            assertEquals(0, mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(),
                     0 /* delta */);
             sGlobalOverridesChecked = true;
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
new file mode 100644
index 0000000..e5f2f89
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.WindowTestRunner;
+import com.android.server.wm.WindowTestsBase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+/**
+ * Test class for [DesktopModeFlagsUtil]
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeFlagsUtilTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeFlagsUtilTest extends WindowTestsBase {
+
+    @Rule
+    public SetFlagsRule setFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        resetCache();
+    }
+
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    @Test
+    @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+        // In absence of dev options, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+
+    @Test
+    @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noOverride_featureFlagOn_returnsTrue() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noOverride_featureFlagOff_returnsFalse() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognized overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognizable overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOn_featureFlagOff_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(
+                OVERRIDE_OFF.getSetting()));
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY,
+                String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY})
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    private void setOverride(Integer setting) {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
+
+        if (setting == null) {
+            Settings.Global.putString(contentResolver, key, null);
+        } else {
+            Settings.Global.putInt(contentResolver, key, setting);
+        }
+    }
+
+    private void resetCache() throws Exception {
+        Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField(
+                "sCachedToggleOverride");
+        cachedToggleOverride.setAccessible(true);
+        cachedToggleOverride.set(null, null);
+
+        // Clear override cache stored in System property
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+    }
+}
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 175a09d..1404413 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.usb;
 
+import com.android.internal.annotations.Keep;
+
 import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE;
 import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST;
 import static android.hardware.usb.UsbPortStatus.MODE_AUDIO_ACCESSORY;
@@ -82,6 +84,7 @@
 import android.util.Slog;
 import android.text.TextUtils;
 
+import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -147,6 +150,8 @@
             "DEVPATH=/devices/virtual/android_usb/android0";
     private static final String ACCESSORY_START_MATCH =
             "DEVPATH=/devices/virtual/misc/usb_accessory";
+    private static final String UDC_SUBSYS_MATCH =
+            "SUBSYSTEM=udc";
     private static final String FUNCTIONS_PATH =
             "/sys/class/android_usb/android0/functions";
     private static final String STATE_PATH =
@@ -226,6 +231,9 @@
 
     private static UsbGadgetHal mUsbGadgetHal;
 
+    private final boolean mEnableUdcSysfsUsbStateUpdate;
+    private String mUdcName = "";
+
     /**
      * Counter for tracking UsbOperation operations.
      */
@@ -260,12 +268,9 @@
                 if (DEBUG) Slog.d(TAG, "sEventLogger == null");
             }
 
-            String state = event.get("USB_STATE");
             String accessory = event.get("ACCESSORY");
 
-            if (state != null) {
-                mHandler.updateState(state);
-            } else if ("GETPROTOCOL".equals(accessory)) {
+            if ("GETPROTOCOL".equals(accessory)) {
                 if (DEBUG) Slog.d(TAG, "got accessory get protocol");
                 mHandler.setAccessoryUEventTime(SystemClock.elapsedRealtime());
                 resetAccessoryHandshakeTimeoutHandler();
@@ -279,6 +284,24 @@
                 mHandler.setStartAccessoryTrue();
                 startAccessoryMode();
             }
+
+            if (mEnableUdcSysfsUsbStateUpdate) {
+                if (!mUdcName.isEmpty()
+                        && "udc".equals(event.get("SUBSYSTEM"))
+                        && event.get("DEVPATH").contains(mUdcName)) {
+                    String action = event.get("ACTION");
+                    if ("add".equals(action)) {
+                        nativeStartGadgetMonitor(mUdcName);
+                    } else if ("remove".equals(action)) {
+                        nativeStopGadgetMonitor();
+                    }
+                }
+            } else {
+                String state = event.get("USB_STATE");
+                if (state != null) {
+                    mHandler.updateState(state);
+                }
+            }
         }
     }
 
@@ -406,9 +429,28 @@
 
         // Watch for USB configuration changes
         mUEventObserver = new UsbUEventObserver();
-        mUEventObserver.startObserving(USB_STATE_MATCH);
         mUEventObserver.startObserving(ACCESSORY_START_MATCH);
 
+        mEnableUdcSysfsUsbStateUpdate =
+                android.hardware.usb.flags.Flags.enableUdcSysfsUsbStateUpdate()
+                && context.getResources().getBoolean(R.bool.config_enableUdcSysfsUsbStateUpdate);
+
+        if (mEnableUdcSysfsUsbStateUpdate) {
+            mUEventObserver.startObserving(UDC_SUBSYS_MATCH);
+            new Thread("GetUsbControllerSysprop") {
+                public void run() {
+                    String udcName;
+                    // blocking wait until usb controller sysprop is available
+                    udcName = nativeWaitAndGetProperty(USB_CONTROLLER_NAME_PROPERTY);
+                    nativeStartGadgetMonitor(udcName);
+                    mUdcName = udcName;
+                    Slog.v(TAG, "USB controller name " + udcName);
+                }
+            }.start();
+        } else {
+            mUEventObserver.startObserving(USB_STATE_MATCH);
+        }
+
         sEventLogger = new EventLogger(DUMPSYS_LOG_BUFFER, "UsbDeviceManager activity");
     }
 
@@ -2609,11 +2651,27 @@
         dump.end(token);
     }
 
+    /**
+     * Update usb state (Called by native code).
+     */
+    @Keep
+    private void updateGadgetState(String state) {
+        Slog.d(TAG, "Usb state update " + state);
+
+        mHandler.updateState(state);
+    }
+
     private native String[] nativeGetAccessoryStrings();
 
     private native ParcelFileDescriptor nativeOpenAccessory();
 
+    private native String nativeWaitAndGetProperty(String propName);
+
     private native FileDescriptor nativeOpenControl(String usbFunction);
 
     private native boolean nativeIsStartRequested();
+
+    private native boolean nativeStartGadgetMonitor(String udcName);
+
+    private native void nativeStopGadgetMonitor();
 }
diff --git a/telephony/OWNERS b/telephony/OWNERS
index 7607c64..92af034 100644
--- a/telephony/OWNERS
+++ b/telephony/OWNERS
@@ -15,4 +15,4 @@
 per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com
 
 #Domain Selection is jointly owned, add additional owners for domain selection specific files
-per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com
+per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,jaesikkong@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
index 2dc8ffb..460de8c 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
@@ -25,4 +25,7 @@
 
     /** carrier id */
     int mCarrierId;
+
+    /** apn */
+    String mNiddApn;
 }
diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
index 08430f2..4143f59 100644
--- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
+++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
@@ -159,7 +159,7 @@
 
     private static BatteryUsageStats buildBatteryUsageStats() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, 0)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0)
                         .setBatteryCapacity(4000)
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index f367c38..06c2651 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -48,6 +48,7 @@
         "testables",
         "testng",
         "truth",
+        "ui-trace-collector",
     ],
     libs: [
         "android.test.mock",
diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml
index 4a99bd4..8c24879 100644
--- a/tests/Input/AndroidTest.xml
+++ b/tests/Input/AndroidTest.xml
@@ -22,6 +22,9 @@
         <option name="shell-timeout" value="660s" />
         <option name="test-timeout" value="600s" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/>
+        <!-- DefaultUITraceListener args -->
+        <option name="instrumentation-arg" key="skip_test_success_metrics" value="true"/>
     </test>
     <object class="com.android.tradefed.testtype.suite.module.TestFailureModuleController"
             type="module_controller">
@@ -32,6 +35,8 @@
         <option name="pull-pattern-keys" value="input_.*" />
         <!-- Pull files created by tests, like the output of screenshot tests -->
         <option name="directory-keys" value="/sdcard/Download/InputTests" />
+        <!-- Pull perfetto traces from DefaultUITraceListener -->
+        <option name="pull-pattern-keys" value="perfetto_file_path*" />
         <option name="collect-on-run-ended-only" value="false" />
     </metrics_collector>
 </configuration>
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java
index be9fb1b..9a062e3 100644
--- a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java
@@ -67,7 +67,7 @@
 
     @Test
     public void allEnabledTraceMode() {
-        final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {});
+        final ProtoLogDataSource ds = new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {});
 
         final ProtoLogDataSource.TlsState tlsState = createTlsState(
                 DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig(
@@ -154,7 +154,7 @@
     private ProtoLogDataSource.TlsState createTlsState(
             DataSourceConfigOuterClass.DataSourceConfig config) {
         final ProtoLogDataSource ds =
-                Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}));
+                Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}));
 
         ProtoInputStream configStream = new ProtoInputStream(config.toByteArray());
         final ProtoLogDataSource.Instance dsInstance = Mockito.spy(
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index e839fc1..7739171 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -137,22 +137,25 @@
       diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg);
       return false;
     }
+
     std::vector<std::string> name_parts = util::Split(flag_name, ':');
     if (name_parts.size() > 2) {
       diag->Error(android::DiagMessage()
                   << "Invalid feature flag and optional value '" << flag_and_value
-                  << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                  << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
       return false;
     }
     flag_name = name_parts[0];
     bool read_only = false;
     if (name_parts.size() == 2) {
-      if (name_parts[1] == "ro") {
+      if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") {
         read_only = true;
+      } else if (name_parts[1] == "READ_WRITE") {
+        read_only = false;
       } else {
         diag->Error(android::DiagMessage()
                     << "Invalid feature flag and optional value '" << flag_and_value
-                    << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                    << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
         return false;
       }
     }
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 35bc637..7818340 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -383,7 +383,7 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics,
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics,
                                          &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
@@ -394,11 +394,11 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_Valid) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics,
-                                         &feature_flag_values));
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux",
+                                         diagnostics, &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
-      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}),
+      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}),
                            Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}),
                            Pair("baz", FeatureFlagProperties{false, std::nullopt}),
                            Pair("quux", FeatureFlagProperties{false, std::nullopt})));
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
index 277a508..5ecf5cf 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
@@ -115,6 +115,9 @@
     /** Creates a {@link AppInfo} from the human-readable DOM element. */
     public AppInfo createFromHrElement(Element appInfoEle, long version)
             throws MalformedXmlException {
+        if (appInfoEle == null) {
+            return null;
+        }
         XmlUtils.throwIfExtraneousAttributes(
                 appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version));
         XmlUtils.throwIfExtraneousChildrenHr(
@@ -184,6 +187,9 @@
     /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */
     public AppInfo createFromOdElement(Element appInfoEle, long version)
             throws MalformedXmlException {
+        if (appInfoEle == null) {
+            return null;
+        }
         XmlUtils.throwIfExtraneousChildrenOd(
                 appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version));
         var requiredOdEles = XmlUtils.getMostRecentVersion(mRequiredOdEles, version);
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
index 14e65e5..e3aa50a 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
@@ -20,8 +20,11 @@
 import com.android.asllib.marshallable.AppInfoTest;
 import com.android.asllib.marshallable.DataLabelsTest;
 import com.android.asllib.marshallable.DataTypeEqualityTest;
+import com.android.asllib.marshallable.DeveloperInfoTest;
 import com.android.asllib.marshallable.SafetyLabelsTest;
+import com.android.asllib.marshallable.SecurityLabelsTest;
 import com.android.asllib.marshallable.SystemAppSafetyLabelTest;
+import com.android.asllib.marshallable.ThirdPartyVerificationTest;
 import com.android.asllib.marshallable.TransparencyInfoTest;
 
 import org.junit.runner.RunWith;
@@ -36,6 +39,9 @@
     DataTypeEqualityTest.class,
     SafetyLabelsTest.class,
     SystemAppSafetyLabelTest.class,
-    TransparencyInfoTest.class
+    TransparencyInfoTest.class,
+    DeveloperInfoTest.class,
+    SecurityLabelsTest.class,
+    ThirdPartyVerificationTest.class
 })
 public class AllTests {}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java
index 283ccbc..6470c06 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java
@@ -28,6 +28,7 @@
 import org.w3c.dom.Element;
 
 import java.nio.file.Paths;
+import java.util.List;
 
 @RunWith(JUnit4.class)
 public class AndroidSafetyLabelTest {
@@ -37,12 +38,16 @@
             "com/android/asllib/androidsafetylabel/od";
 
     private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml";
-    private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml";
+    private static final String VALID_V2_FILE_NAME = "valid-empty.xml";
+    private static final String VALID_V1_FILE_NAME = "valid-v1.xml";
     private static final String WITH_SAFETY_LABELS_FILE_NAME = "with-safety-labels.xml";
     private static final String WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME =
             "with-system-app-safety-label.xml";
     private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml";
 
+    public static final List<String> REQUIRED_FIELD_NAMES_OD_V2 =
+            List.of("system_app_safety_label", "transparency_info");
+
     @Before
     public void setUp() throws Exception {
         System.out.println("set up.");
@@ -56,12 +61,12 @@
         odToHrExpectException(MISSING_VERSION_FILE_NAME);
     }
 
-    /** Test for android safety label valid empty. */
+    /** Test for android safety label valid v2. */
     @Test
-    public void testAndroidSafetyLabelValidEmptyFile() throws Exception {
-        System.out.println("starting testAndroidSafetyLabelValidEmptyFile.");
-        testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME);
-        testOdToHrAndroidSafetyLabel(VALID_EMPTY_FILE_NAME);
+    public void testAndroidSafetyLabelValidV2File() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelValidV2File.");
+        testHrToOdAndroidSafetyLabel(VALID_V2_FILE_NAME);
+        testOdToHrAndroidSafetyLabel(VALID_V2_FILE_NAME);
     }
 
     /** Test for android safety label with safety labels. */
@@ -72,6 +77,34 @@
         testOdToHrAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME);
     }
 
+    /** Tests missing required fields fails, V2. */
+    @Test
+    public void testMissingRequiredFieldsOdV2() throws Exception {
+        for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) {
+            System.out.println("testing missing required field od v2: " + reqField);
+            var ele =
+                    TestUtils.getElementFromResource(
+                            Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V2_FILE_NAME));
+            TestUtils.removeOdChildEleWithName(ele, reqField);
+            assertThrows(
+                    MalformedXmlException.class,
+                    () -> new AndroidSafetyLabelFactory().createFromOdElement(ele));
+        }
+    }
+
+    /** Tests missing optional fields succeeds, V1. */
+    @Test
+    public void testMissingOptionalFieldsOdV1() throws Exception {
+        for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) {
+            System.out.println("testing missing optional field od v1: " + reqField);
+            var ele =
+                    TestUtils.getElementFromResource(
+                            Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V1_FILE_NAME));
+            TestUtils.removeOdChildEleWithName(ele, reqField);
+            var unused = new AndroidSafetyLabelFactory().createFromOdElement(ele);
+        }
+    }
+
     private void hrToOdExpectException(String fileName) {
         assertThrows(
                 MalformedXmlException.class,
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java
index b557fea..cc58a61 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java
@@ -35,8 +35,6 @@
 
 @RunWith(JUnit4.class)
 public class DataLabelsTest {
-    private static final long DEFAULT_VERSION = 2L;
-
     private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr";
     private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od";
 
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java
new file mode 100644
index 0000000..a4472b1
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.asllib.testutils.TestUtils;
+import com.android.asllib.util.MalformedXmlException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Element;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DeveloperInfoTest {
+    private static final String DEVELOPER_INFO_HR_PATH = "com/android/asllib/developerinfo/hr";
+    private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od";
+    public static final List<String> REQUIRED_FIELD_NAMES =
+            List.of("address", "countryRegion", "email", "name", "relationship");
+    public static final List<String> REQUIRED_FIELD_NAMES_OD =
+            List.of("address", "country_region", "email", "name", "relationship");
+    public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId");
+    public static final List<String> OPTIONAL_FIELD_NAMES_OD =
+            List.of("website", "app_developer_registry_id");
+
+    private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml";
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+    }
+
+    /** Test for all fields valid. */
+    @Test
+    public void testAllFieldsValid() throws Exception {
+        System.out.println("starting testAllFieldsValid.");
+        testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME);
+        testOdToHrDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME);
+    }
+
+    /** Tests missing required fields fails. */
+    @Test
+    public void testMissingRequiredFields() throws Exception {
+        System.out.println("Starting testMissingRequiredFields");
+        for (String reqField : REQUIRED_FIELD_NAMES) {
+            System.out.println("testing missing required field: " + reqField);
+            var developerInfoEle =
+                    TestUtils.getElementFromResource(
+                            Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            developerInfoEle.removeAttribute(reqField);
+
+            assertThrows(
+                    MalformedXmlException.class,
+                    () -> new DeveloperInfoFactory().createFromHrElement(developerInfoEle));
+        }
+
+        for (String reqField : REQUIRED_FIELD_NAMES_OD) {
+            System.out.println("testing missing required field od: " + reqField);
+            var developerInfoEle =
+                    TestUtils.getElementFromResource(
+                            Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            TestUtils.removeOdChildEleWithName(developerInfoEle, reqField);
+
+            assertThrows(
+                    MalformedXmlException.class,
+                    () -> new DeveloperInfoFactory().createFromOdElement(developerInfoEle));
+        }
+    }
+
+    /** Tests missing optional fields passes. */
+    @Test
+    public void testMissingOptionalFields() throws Exception {
+        for (String optField : OPTIONAL_FIELD_NAMES) {
+            var developerInfoEle =
+                    TestUtils.getElementFromResource(
+                            Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            developerInfoEle.removeAttribute(optField);
+            DeveloperInfo developerInfo =
+                    new DeveloperInfoFactory().createFromHrElement(developerInfoEle);
+            developerInfo.toOdDomElement(TestUtils.document());
+        }
+
+        for (String optField : OPTIONAL_FIELD_NAMES_OD) {
+            var developerInfoEle =
+                    TestUtils.getElementFromResource(
+                            Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            TestUtils.removeOdChildEleWithName(developerInfoEle, optField);
+            DeveloperInfo developerInfo =
+                    new DeveloperInfoFactory().createFromOdElement(developerInfoEle);
+            developerInfo.toHrDomElement(TestUtils.document());
+        }
+    }
+
+    private void testHrToOdDeveloperInfo(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        DeveloperInfo developerInfo =
+                new DeveloperInfoFactory()
+                        .createFromHrElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(DEVELOPER_INFO_HR_PATH, fileName)));
+        Element developerInfoEle = developerInfo.toOdDomElement(doc);
+        doc.appendChild(developerInfoEle);
+        TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_OD_PATH, fileName));
+    }
+
+    private void testOdToHrDeveloperInfo(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        DeveloperInfo developerInfo =
+                new DeveloperInfoFactory()
+                        .createFromOdElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(DEVELOPER_INFO_OD_PATH, fileName)));
+        Element developerInfoEle = developerInfo.toHrDomElement(doc);
+        doc.appendChild(developerInfoEle);
+        TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_HR_PATH, fileName));
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java
index 7cd510f..fc8ff00 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java
@@ -26,13 +26,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.w3c.dom.Element;
-import org.xml.sax.SAXException;
 
-import java.io.IOException;
 import java.nio.file.Paths;
 
-import javax.xml.parsers.ParserConfigurationException;
-
 @RunWith(JUnit4.class)
 public class SafetyLabelsTest {
     private static final long DEFAULT_VERSION = 2L;
@@ -42,6 +38,8 @@
 
     private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml";
     private static final String WITH_DATA_LABELS_FILE_NAME = "with-data-labels.xml";
+    private static final String VALID_V1_FILE_NAME = "valid-v1.xml";
+    private static final String UNRECOGNIZED_FIELD_V2_FILE_NAME = "unrecognized-field-v2.xml";
 
     @Before
     public void setUp() throws Exception {
@@ -52,61 +50,59 @@
     @Test
     public void testSafetyLabelsValidEmptyFile() throws Exception {
         System.out.println("starting testSafetyLabelsValidEmptyFile.");
-        testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME);
-        testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME);
+        testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION);
+        testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION);
     }
 
     /** Test for safety labels with data labels. */
     @Test
     public void testSafetyLabelsWithDataLabels() throws Exception {
         System.out.println("starting testSafetyLabelsWithDataLabels.");
-        testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME);
-        testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME);
+        testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION);
+        testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION);
     }
 
-    private void hrToOdExpectException(String fileName)
-            throws ParserConfigurationException, IOException, SAXException {
-        var safetyLabelsEle =
-                TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_HR_PATH, fileName));
+    /** Tests valid fields v1. */
+    @Test
+    public void testValidFieldsV1() throws Exception {
+        var ele =
+                TestUtils.getElementFromResource(
+                        Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME));
+        var unused = new SafetyLabelsFactory().createFromOdElement(ele, 1L);
+    }
+
+    /** Tests unrecognized field v2. */
+    @Test
+    public void testUnrecognizedFieldV2() throws Exception {
+        var ele =
+                TestUtils.getElementFromResource(
+                        Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME));
         assertThrows(
                 MalformedXmlException.class,
-                () ->
-                        new SafetyLabelsFactory()
-                                .createFromHrElement(safetyLabelsEle, DEFAULT_VERSION));
+                () -> new SafetyLabelsFactory().createFromOdElement(ele, 2L));
     }
 
-    private void odToHrExpectException(String fileName)
-            throws ParserConfigurationException, IOException, SAXException {
-        var safetyLabelsEle =
-                TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_OD_PATH, fileName));
-        assertThrows(
-                MalformedXmlException.class,
-                () ->
-                        new SafetyLabelsFactory()
-                                .createFromOdElement(safetyLabelsEle, DEFAULT_VERSION));
-    }
-
-    private void testHrToOdSafetyLabels(String fileName) throws Exception {
+    private void testHrToOdSafetyLabels(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         SafetyLabels safetyLabels =
                 new SafetyLabelsFactory()
                         .createFromHrElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(SAFETY_LABELS_HR_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element appInfoEle = safetyLabels.toOdDomElement(doc);
         doc.appendChild(appInfoEle);
         TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_OD_PATH, fileName));
     }
 
-    private void testOdToHrSafetyLabels(String fileName) throws Exception {
+    private void testOdToHrSafetyLabels(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         SafetyLabels safetyLabels =
                 new SafetyLabelsFactory()
                         .createFromOdElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(SAFETY_LABELS_OD_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element appInfoEle = safetyLabels.toHrDomElement(doc);
         doc.appendChild(appInfoEle);
         TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_HR_PATH, fileName));
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java
new file mode 100644
index 0000000..9d197a2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Element;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class SecurityLabelsTest {
+    private static final String SECURITY_LABELS_HR_PATH = "com/android/asllib/securitylabels/hr";
+    private static final String SECURITY_LABELS_OD_PATH = "com/android/asllib/securitylabels/od";
+
+    public static final List<String> OPTIONAL_FIELD_NAMES =
+            List.of("isDataDeletable", "isDataEncrypted");
+    public static final List<String> OPTIONAL_FIELD_NAMES_OD =
+            List.of("is_data_deletable", "is_data_encrypted");
+
+    private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml";
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+    }
+
+    /** Test for all fields valid. */
+    @Test
+    public void testAllFieldsValid() throws Exception {
+        System.out.println("starting testAllFieldsValid.");
+        testHrToOdSecurityLabels(ALL_FIELDS_VALID_FILE_NAME);
+        testOdToHrSecurityLabels(ALL_FIELDS_VALID_FILE_NAME);
+    }
+
+    /** Tests missing optional fields passes. */
+    @Test
+    public void testMissingOptionalFields() throws Exception {
+        for (String optField : OPTIONAL_FIELD_NAMES) {
+            var ele =
+                    TestUtils.getElementFromResource(
+                            Paths.get(SECURITY_LABELS_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            ele.removeAttribute(optField);
+            SecurityLabels securityLabels = new SecurityLabelsFactory().createFromHrElement(ele);
+            securityLabels.toOdDomElement(TestUtils.document());
+        }
+        for (String optField : OPTIONAL_FIELD_NAMES_OD) {
+            var ele =
+                    TestUtils.getElementFromResource(
+                            Paths.get(SECURITY_LABELS_OD_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            TestUtils.removeOdChildEleWithName(ele, optField);
+            SecurityLabels securityLabels = new SecurityLabelsFactory().createFromOdElement(ele);
+            securityLabels.toHrDomElement(TestUtils.document());
+        }
+    }
+
+    private void testHrToOdSecurityLabels(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        SecurityLabels securityLabels =
+                new SecurityLabelsFactory()
+                        .createFromHrElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(SECURITY_LABELS_HR_PATH, fileName)));
+        Element ele = securityLabels.toOdDomElement(doc);
+        doc.appendChild(ele);
+        TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_OD_PATH, fileName));
+    }
+
+    private void testOdToHrSecurityLabels(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        SecurityLabels securityLabels =
+                new SecurityLabelsFactory()
+                        .createFromOdElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(SECURITY_LABELS_OD_PATH, fileName)));
+        Element ele = securityLabels.toHrDomElement(doc);
+        doc.appendChild(ele);
+        TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_HR_PATH, fileName));
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java
index 9dcc652..04bcd78 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java
@@ -43,6 +43,7 @@
             "com/android/asllib/systemappsafetylabel/od";
 
     private static final String VALID_FILE_NAME = "valid.xml";
+    private static final String VALID_V1_FILE_NAME = "valid-v1.xml";
     private static final String MISSING_BOOL_FILE_NAME = "missing-bool.xml";
 
     /** Logic for setting up tests (empty if not yet needed). */
@@ -57,59 +58,81 @@
     @Test
     public void testValid() throws Exception {
         System.out.println("starting testValid.");
-        testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME);
-        testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME);
+        testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION);
+        testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION);
+    }
+
+    /** Test for valid v1. */
+    @Test
+    public void testValidV1() throws Exception {
+        System.out.println("starting testValidV1.");
+        var doc = TestUtils.document();
+        var unused =
+                new SystemAppSafetyLabelFactory()
+                        .createFromOdElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(
+                                                SYSTEM_APP_SAFETY_LABEL_OD_PATH,
+                                                VALID_V1_FILE_NAME)),
+                                1L);
+    }
+
+    /** Test for testV1InvalidAsV2. */
+    @Test
+    public void testV1InvalidAsV2() throws Exception {
+        System.out.println("starting testV1InvalidAsV2.");
+        odToHrExpectException(VALID_V1_FILE_NAME, 2L);
     }
 
     /** Tests missing bool. */
     @Test
     public void testMissingBool() throws Exception {
         System.out.println("starting testMissingBool.");
-        hrToOdExpectException(MISSING_BOOL_FILE_NAME);
-        odToHrExpectException(MISSING_BOOL_FILE_NAME);
+        hrToOdExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION);
+        odToHrExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION);
     }
 
-    private void hrToOdExpectException(String fileName)
+    private void hrToOdExpectException(String fileName, long version)
             throws ParserConfigurationException, IOException, SAXException {
         var ele =
                 TestUtils.getElementFromResource(
                         Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName));
         assertThrows(
                 MalformedXmlException.class,
-                () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, DEFAULT_VERSION));
+                () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, version));
     }
 
-    private void odToHrExpectException(String fileName)
+    private void odToHrExpectException(String fileName, long version)
             throws ParserConfigurationException, IOException, SAXException {
         var ele =
                 TestUtils.getElementFromResource(
                         Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName));
         assertThrows(
                 MalformedXmlException.class,
-                () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, DEFAULT_VERSION));
+                () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, version));
     }
 
-    private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception {
+    private void testHrToOdSystemAppSafetyLabel(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         SystemAppSafetyLabel systemAppSafetyLabel =
                 new SystemAppSafetyLabelFactory()
                         .createFromHrElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element resultingEle = systemAppSafetyLabel.toOdDomElement(doc);
         doc.appendChild(resultingEle);
         TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName));
     }
 
-    private void testOdToHrSystemAppSafetyLabel(String fileName) throws Exception {
+    private void testOdToHrSystemAppSafetyLabel(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         SystemAppSafetyLabel systemAppSafetyLabel =
                 new SystemAppSafetyLabelFactory()
                         .createFromOdElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element resultingEle = systemAppSafetyLabel.toHrDomElement(doc);
         doc.appendChild(resultingEle);
         TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName));
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java
new file mode 100644
index 0000000..ebb2e93
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.asllib.testutils.TestUtils;
+import com.android.asllib.util.MalformedXmlException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Element;
+
+import java.nio.file.Paths;
+
+@RunWith(JUnit4.class)
+public class ThirdPartyVerificationTest {
+    private static final String THIRD_PARTY_VERIFICATION_HR_PATH =
+            "com/android/asllib/thirdpartyverification/hr";
+    private static final String THIRD_PARTY_VERIFICATION_OD_PATH =
+            "com/android/asllib/thirdpartyverification/od";
+
+    private static final String VALID_FILE_NAME = "valid.xml";
+    private static final String MISSING_URL_FILE_NAME = "missing-url.xml";
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+    }
+
+    /** Test for valid. */
+    @Test
+    public void testValid() throws Exception {
+        System.out.println("starting testValid.");
+        testHrToOdThirdPartyVerification(VALID_FILE_NAME);
+        testOdToHrThirdPartyVerification(VALID_FILE_NAME);
+    }
+
+    /** Tests missing url. */
+    @Test
+    public void testMissingUrl() throws Exception {
+        System.out.println("starting testMissingUrl.");
+        hrToOdExpectException(MISSING_URL_FILE_NAME);
+        odToHrExpectException(MISSING_URL_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        assertThrows(
+                MalformedXmlException.class,
+                () -> {
+                    new ThirdPartyVerificationFactory()
+                            .createFromHrElement(
+                                    TestUtils.getElementFromResource(
+                                            Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName)));
+                });
+    }
+
+    private void odToHrExpectException(String fileName) {
+        assertThrows(
+                MalformedXmlException.class,
+                () -> {
+                    new ThirdPartyVerificationFactory()
+                            .createFromOdElement(
+                                    TestUtils.getElementFromResource(
+                                            Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName)));
+                });
+    }
+
+    private void testHrToOdThirdPartyVerification(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        ThirdPartyVerification thirdPartyVerification =
+                new ThirdPartyVerificationFactory()
+                        .createFromHrElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName)));
+        Element ele = thirdPartyVerification.toOdDomElement(doc);
+        doc.appendChild(ele);
+        TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName));
+    }
+
+    private void testOdToHrThirdPartyVerification(String fileName) throws Exception {
+        var doc = TestUtils.document();
+        ThirdPartyVerification thirdPartyVerification =
+                new ThirdPartyVerificationFactory()
+                        .createFromOdElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName)));
+        Element ele = thirdPartyVerification.toHrDomElement(doc);
+        doc.appendChild(ele);
+        TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName));
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java
index 6547fb9..b27d6dd 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java
@@ -16,16 +16,23 @@
 
 package com.android.asllib.marshallable;
 
+import static org.junit.Assert.assertThrows;
+
 import com.android.asllib.testutils.TestUtils;
+import com.android.asllib.util.MalformedXmlException;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
 
+import java.io.IOException;
 import java.nio.file.Paths;
 
+import javax.xml.parsers.ParserConfigurationException;
+
 @RunWith(JUnit4.class)
 public class TransparencyInfoTest {
     private static final long DEFAULT_VERSION = 2L;
@@ -35,6 +42,10 @@
     private static final String TRANSPARENCY_INFO_OD_PATH =
             "com/android/asllib/transparencyinfo/od";
     private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml";
+    private static final String VALID_EMPTY_V1_FILE_NAME = "valid-empty-v1.xml";
+    private static final String VALID_DEV_INFO_V1_FILE_NAME = "valid-dev-info-v1.xml";
+    private static final String WITH_APP_INFO_AND_DEV_INFO_FILE_NAME =
+            "with-app-info-v2-and-dev-info-v1.xml";
 
     @Before
     public void setUp() throws Exception {
@@ -45,33 +56,78 @@
     @Test
     public void testTransparencyInfoWithAppInfo() throws Exception {
         System.out.println("starting testTransparencyInfoWithAppInfo.");
-        testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME);
-        testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME);
+        testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION);
+        testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION);
     }
 
-    private void testHrToOdTransparencyInfo(String fileName) throws Exception {
+    /** Test for testMissingAppInfoFailsInV2. */
+    @Test
+    public void testMissingAppInfoFailsInV2() throws Exception {
+        System.out.println("starting testMissingAppInfoFailsInV2.");
+        odToHrExpectException(VALID_EMPTY_V1_FILE_NAME, 2L);
+    }
+
+    /** Test for testMissingAppInfoPassesInV1. */
+    @Test
+    public void testMissingAppInfoPassesInV1() throws Exception {
+        System.out.println("starting testMissingAppInfoPassesInV1.");
+        testParseOdTransparencyInfo(VALID_EMPTY_V1_FILE_NAME, 1L);
+    }
+
+    /** Test for testDeveloperInfoExistencePassesInV1. */
+    @Test
+    public void testDeveloperInfoExistencePassesInV1() throws Exception {
+        System.out.println("starting testDeveloperInfoExistencePassesInV1.");
+        testParseOdTransparencyInfo(VALID_DEV_INFO_V1_FILE_NAME, 1L);
+    }
+
+    /** Test for testDeveloperInfoExistenceFailsInV2. */
+    @Test
+    public void testDeveloperInfoExistenceFailsInV2() throws Exception {
+        System.out.println("starting testDeveloperInfoExistenceFailsInV2.");
+        odToHrExpectException(WITH_APP_INFO_AND_DEV_INFO_FILE_NAME, 2L);
+    }
+
+    private void testHrToOdTransparencyInfo(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         TransparencyInfo transparencyInfo =
                 new TransparencyInfoFactory()
                         .createFromHrElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element resultingEle = transparencyInfo.toOdDomElement(doc);
         doc.appendChild(resultingEle);
         TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName));
     }
 
-    private void testOdToHrTransparencyInfo(String fileName) throws Exception {
+    private void testParseOdTransparencyInfo(String fileName, long version) throws Exception {
+        var unused =
+                new TransparencyInfoFactory()
+                        .createFromOdElement(
+                                TestUtils.getElementFromResource(
+                                        Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)),
+                                version);
+    }
+
+    private void testOdToHrTransparencyInfo(String fileName, long version) throws Exception {
         var doc = TestUtils.document();
         TransparencyInfo transparencyInfo =
                 new TransparencyInfoFactory()
                         .createFromOdElement(
                                 TestUtils.getElementFromResource(
                                         Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)),
-                                DEFAULT_VERSION);
+                                version);
         Element resultingEle = transparencyInfo.toHrDomElement(doc);
         doc.appendChild(resultingEle);
         TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName));
     }
+
+    private void odToHrExpectException(String fileName, long version)
+            throws ParserConfigurationException, IOException, SAXException {
+        var ele = TestUtils.getElementFromResource(Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName));
+        assertThrows(
+                MalformedXmlException.class,
+                () -> new TransparencyInfoFactory().createFromOdElement(ele, version));
+    }
 }
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml
new file mode 100644
index 0000000..7e984e3
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml
@@ -0,0 +1,8 @@
+<bundle>
+    <long name="version" value="1"/>
+    <pbundle_as_map name="system_app_safety_label">
+        <string name="url" value="www.example.com"/>
+    </pbundle_as_map>
+    <pbundle_as_map name="transparency_info">
+    </pbundle_as_map>
+</bundle>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml
index 810078e..01fd718 100644
--- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml
@@ -21,5 +21,5 @@
     <string name="category" value="Food and drink"/>
     <string name="email" value="max@maxloh.com"/>
     <string name="website" value="www.example.com"/>
-    <string name="unrecognized" value="www.example.com"/>
+    <boolean name="aps_compliant" value="false"/>
 </pbundle_as_map>
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml
new file mode 100644
index 0000000..1384a2f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml
@@ -0,0 +1,9 @@
+<pbundle_as_map name="safety_labels">
+    <pbundle_as_map name="security_labels">
+        <boolean name="is_data_deletable" value="true" />
+        <boolean name="is_data_encrypted" value="false" />
+    </pbundle_as_map>
+    <pbundle_as_map name="third_party_verification">
+        <string name="url" value="www.example.com"/>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml
new file mode 100644
index 0000000..f96535b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml
@@ -0,0 +1,3 @@
+<pbundle_as_map name="system_app_safety_label">
+    <string name="url" value="www.example.com"/>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml
new file mode 100644
index 0000000..d7a4e1a
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml
@@ -0,0 +1,12 @@
+
+<pbundle_as_map name="transparency_info">
+    <pbundle_as_map name="developer_info">
+        <string name="name" value="max"/>
+        <string name="email" value="max@example.com"/>
+        <string name="address" value="111 blah lane"/>
+        <string name="country_region" value="US"/>
+        <long name="relationship" value="5"/>
+        <string name="website" value="example.com"/>
+        <string name="app_developer_registry_id" value="registry_id"/>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml